diff --git a/.gitignore b/.gitignore index 569e43ac..93a9b60f 100644 --- a/.gitignore +++ b/.gitignore @@ -235,6 +235,7 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ +/*/**/package-lock.json # Typescript v1 declaration files typings/ @@ -287,8 +288,6 @@ __pycache__/ *.odx.cs *.xsd.cs - -Build/nuget.exe *.js *.map diff --git a/Build/MQTTnet.AspNetCore.nuspec b/Build/MQTTnet.AspNetCore.nuspec index 241582a2..a46cd815 100644 --- a/Build/MQTTnet.AspNetCore.nuspec +++ b/Build/MQTTnet.AspNetCore.nuspec @@ -5,23 +5,28 @@ 0.0.0 Christian Kratky Christian Kratky - https://github.com/chkr1011/MQTTnet/blob/master/LICENSE + LICENSE https://github.com/chkr1011/MQTTnet https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is a support library to integrate MQTTnet into AspNetCore. For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2018 + Copyright Christian Kratky 2016-2019 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 - + + + + + + diff --git a/Build/MQTTnet.Extensions.ManagedClient.nuspec b/Build/MQTTnet.Extensions.ManagedClient.nuspec index af18c0f6..70fcd44b 100644 --- a/Build/MQTTnet.Extensions.ManagedClient.nuspec +++ b/Build/MQTTnet.Extensions.ManagedClient.nuspec @@ -5,20 +5,23 @@ 0.0.0 Christian Kratky Christian Kratky - https://github.com/chkr1011/MQTTnet/blob/master/LICENSE + LICENSE https://github.com/chkr1011/MQTTnet https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is an extension library which provides a managed MQTT client with additional features using MQTTnet. For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2018 + Copyright Christian Kratky 2016-2019 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 - + + + + @@ -37,4 +40,4 @@ - \ No newline at end of file + diff --git a/Build/MQTTnet.Extensions.Rpc.nuspec b/Build/MQTTnet.Extensions.Rpc.nuspec index 2a443728..13d3d4c1 100644 --- a/Build/MQTTnet.Extensions.Rpc.nuspec +++ b/Build/MQTTnet.Extensions.Rpc.nuspec @@ -5,20 +5,23 @@ 0.0.0 Christian Kratky Christian Kratky - https://github.com/chkr1011/MQTTnet/blob/master/LICENSE + LICENSE https://github.com/chkr1011/MQTTnet https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false This is an extension library which allows executing synchronous device calls including a response using MQTTnet. For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2018 + Copyright Christian Kratky 2016-2019 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 - + + + + @@ -37,4 +40,4 @@ - \ No newline at end of file + diff --git a/Build/MQTTnet.NETStandard.nuspec b/Build/MQTTnet.NETStandard.nuspec new file mode 100644 index 00000000..492532dd --- /dev/null +++ b/Build/MQTTnet.NETStandard.nuspec @@ -0,0 +1,42 @@ + + + + MQTTnet.NETStandard + 0.0.0 + Christian Kratky + Christian Kratky + LICENSE + https://github.com/chkr1011/MQTTnet + https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png + false + MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). This package contains the .NET Standard version of the library only. + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). + Copyright Christian Kratky 2016-2019 + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Build/MQTTnet.nuspec b/Build/MQTTnet.nuspec index 8ac9e692..c2f91e42 100644 --- a/Build/MQTTnet.nuspec +++ b/Build/MQTTnet.nuspec @@ -5,15 +5,44 @@ 0.0.0 Christian Kratky Christian Kratky - https://github.com/chkr1011/MQTTnet/blob/master/LICENSE + LICENSE https://github.com/chkr1011/MQTTnet https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png false MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). - * [Core] Fixed a memory leak when processing lots of messages (thanks to @tschanko) + +* [Core] Added support for MQTTv5 packages. +* [Core] Performance improvements. +* [Core] Removed obsolete methods. +* [Core] Fixed a memory leak when processing lots of messages (thanks to @tschanko) +* [Core] Added more overloads for MQTT factory. +* [Core] The client password is now hidden from the logs (replaced with **** if set). +* [Core] Fixed a memory leak when using SSL connections (thanks to @biovoid). +* [Client] Added validation of topics before publishing. +* [Client] Added new MQTTv5 features to options builder. +* [Client] Added uniform API across all supported MQTT versions (BREAKING CHANGE!) +* [Client] The client will now avoid sending an ACK if an exception has been thrown in message handler (thanks to @ramonsmits). +* [Client] Fixed issues in QoS 2 handling which leads to message loss. +* [Client] Replaced all events with proper async compatible handlers (BREAKING CHANGE!). +* [Client] Added more configuration values to TCP endpoint options. +* [Client] Added used PacketIdentifier to publish result. +* [ManagedClient] Replaced all events with proper async compatible handlers (BREAKING CHANGE!). +* [ManagedClient] The log ID is now propagated to the internal client (thanks to @vbBerni). +* [ManagedClient] Added validation of topics before publishing. +* [ManagedClient] The internal MQTT client is now closed properly (thanks to @vbBerni). +* [Server] Added support for MQTTv5 clients. The server will still return _success_ for all cases at the moment even if more granular codes are available. +* [Server] Fixed issues in QoS 2 handling which leads to message loss. +* [Server] Replaced all events with proper async compatible handlers (BREAKING CHANGE!). +* [Server] The used logger instance is now propagated to the WebSocket server adapter. +* [Server] Added the flag "IsSecureConnection" which is set to true when the connection is encrypted. * [Server] Fixed wrong will message behavior when stopping server (thanks to @JohBa) +* [Server] Added validation of topics before publishing. +* [Server] Added more configuration values to TCP endpoint options. +* [MQTTnet Server] Added as first Alpha version of standalone cross platform MQTT server. + +* [Note] Due to MQTTv5 a lot of new classes were introduced. This required adding new namespaces as well. Most classes are backward compatible but new namespaces must be added. - Copyright Christian Kratky 2016-2018 + Copyright Christian Kratky 2016-2019 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 @@ -29,7 +58,7 @@ - + @@ -39,6 +68,9 @@ + + + diff --git a/Build/build.ps1 b/Build/build.ps1 index 63bae548..3f5c487d 100644 --- a/Build/build.ps1 +++ b/Build/build.ps1 @@ -3,45 +3,66 @@ param([string]$assemblyVersion, [string]$nugetVersion) if ([string]::IsNullOrEmpty($assemblyVersion)) {$assemblyVersion = "0.0.1"} if ([string]::IsNullOrEmpty($nugetVersion)) {$nugetVersion = "0.0.1"} +$vswhere = ${Env:\ProgramFiles(x86)} + '\Microsoft Visual Studio\Installer\vswhere' +$msbuild = &$vswhere -products * -requires Microsoft.Component.MSBuild -latest -find MSBuild\**\Bin\MSBuild.exe + +Write-Host Write-Host "Assembly version = $assemblyVersion" Write-Host "Nuget version = $nugetVersion" +Write-Host "MSBuild path = $msbuild" Write-Host -$vswhere = ${Env:\ProgramFiles(x86)} + '\Microsoft Visual Studio\Installer\vswhere' +# Build the core library +&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -$path = &$vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath -if ($path) { - $msbuild = join-path $path 'MSBuild\15.0\Bin\MSBuild.exe' +# Build the ASP.NET Core 2.0 extension +&$msbuild ..\Source\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - # Build the core library - &$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - - # Build the ASP.NET Core 2.0 extension - &$msbuild ..\Source\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +# Build the RPC extension +&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - # Build the RPC extension - &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +# Build the Managed Client extension +&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - # Build the Managed Client extension - &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - &$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" +# Build MQTTnet.Server +Remove-Item ..\Source\MQTTnet.Server\bin\Release\netcoreapp2.2 -Recurse -Force -ErrorAction SilentlyContinue +&$msbuild ..\Source\MQTTnet.Server\MQTTnet.Server.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netcoreapp2.2" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue +$source = "..\Source\MQTTnet.Server\bin\Release\netcoreapp2.2" +$destination = "..\Source\MQTTnet.Server\bin\MQTTnet.Server-Portable-v$nugetVersion.zip" +If(Test-path $destination) {Remove-item $destination} + Add-Type -assembly "system.io.compression.filesystem" +[io.compression.zipfile]::CreateFromDirectory($source, $destination) - New-Item -ItemType Directory -Force -Path .\NuGet - .\nuget.exe pack MQTTnet.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion - .\nuget.exe pack MQTTnet.AspNetCore.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion - .\nuget.exe pack MQTTnet.Extensions.Rpc.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion - .\nuget.exe pack MQTTnet.Extensions.ManagedClient.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion -} \ No newline at end of file +# Create NuGet packages. +Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue + +Copy-Item MQTTnet.AspNetCore.nuspec -Destination MQTTnet.AspNetCore.nuspec.old -Force +(Get-Content MQTTnet.AspNetCore.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.AspNetCore.nuspec +Copy-Item MQTTnet.Extensions.Rpc.nuspec -Destination MQTTnet.Extensions.Rpc.nuspec.old -Force +(Get-Content MQTTnet.Extensions.Rpc.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.Extensions.Rpc.nuspec +Copy-Item MQTTnet.Extensions.ManagedClient.nuspec -Destination MQTTnet.Extensions.ManagedClient.nuspec.old -Force +(Get-Content MQTTnet.Extensions.ManagedClient.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.Extensions.ManagedClient.nuspec + +New-Item -ItemType Directory -Force -Path .\NuGet +.\nuget.exe pack MQTTnet.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion +.\nuget.exe pack MQTTnet.NETStandard.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion +.\nuget.exe pack MQTTnet.AspNetCore.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion +.\nuget.exe pack MQTTnet.Extensions.Rpc.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion +.\nuget.exe pack MQTTnet.Extensions.ManagedClient.nuspec -Verbosity detailed -Symbols -OutputDir "NuGet" -Version $nugetVersion + +Move-Item MQTTnet.AspNetCore.nuspec.old -Destination MQTTnet.AspNetCore.nuspec -Force +Move-Item MQTTnet.Extensions.Rpc.nuspec.old -Destination MQTTnet.Extensions.Rpc.nuspec -Force +Move-Item MQTTnet.Extensions.ManagedClient.nuspec.old -Destination MQTTnet.Extensions.ManagedClient.nuspec -Force \ No newline at end of file diff --git a/Build/nuget.exe b/Build/nuget.exe new file mode 100644 index 00000000..c96819b5 Binary files /dev/null and b/Build/nuget.exe differ diff --git a/Images/Logo_128x128.png b/Images/Logo_128x128.png index a528d7ce..697acbf2 100644 Binary files a/Images/Logo_128x128.png and b/Images/Logo_128x128.png differ diff --git a/LICENSE b/LICENSE index 7d84e2b1..784616d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Christian Kratky +MQTTnet Copyright (c) 2016-2019 Christian Kratky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MQTTnet.noUWP.sln b/MQTTnet.noUWP.sln index 8d9130ed..1d24ba22 100644 --- a/MQTTnet.noUWP.sln +++ b/MQTTnet.noUWP.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27004.2010 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Core.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Core.Tests.csproj", "{A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9248C2E1-B9D6-40BF-81EC-86004D7765B4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{32A630A7-2598-41D7-B625-204CD906F5FB}" @@ -45,6 +43,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Tests\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Tests.csproj", "{9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Server", "Source\MQTTnet.Server\MQTTnet.Server.csproj", "{DADEA836-5CC3-474C-A2D8-4D0F31118CD7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,22 +59,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|ARM.ActiveCfg = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|ARM.Build.0 = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|x64.ActiveCfg = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|x64.Build.0 = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|x86.ActiveCfg = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Debug|x86.Build.0 = Debug|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|Any CPU.Build.0 = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|ARM.ActiveCfg = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|ARM.Build.0 = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x64.ActiveCfg = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x64.Build.0 = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x86.ActiveCfg = Release|Any CPU - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}.Release|x86.Build.0 = Release|Any CPU {3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Debug|ARM.ActiveCfg = Debug|Any CPU @@ -201,12 +187,43 @@ Global {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x64.Build.0 = Release|Any CPU {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.ActiveCfg = Release|Any CPU {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.Build.0 = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|ARM.ActiveCfg = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|ARM.Build.0 = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|x64.Build.0 = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Debug|x86.Build.0 = Debug|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|Any CPU.Build.0 = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|ARM.ActiveCfg = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|ARM.Build.0 = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|x64.ActiveCfg = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|x64.Build.0 = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|x86.ActiveCfg = Release|Any CPU + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41}.Release|x86.Build.0 = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|ARM.Build.0 = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|x64.ActiveCfg = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|x64.Build.0 = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|x86.ActiveCfg = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Debug|x86.Build.0 = Debug|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|Any CPU.Build.0 = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|ARM.ActiveCfg = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|ARM.Build.0 = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|x64.ActiveCfg = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|x64.Build.0 = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|x86.ActiveCfg = Release|Any CPU + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {3587E506-55A2-4EB3-99C7-DC01E42D25D2} = {32A630A7-2598-41D7-B625-204CD906F5FB} {3D283AAD-AAA8-4339-8394-52F80B6304DB} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {C6FF8AEA-0855-41EC-A1F3-AC262225BAB9} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} @@ -215,6 +232,8 @@ Global {998D04DD-7CB0-45F5-A393-E2495C16399E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {C400533A-8EBA-4F0B-BF4D-295C3708604B} = {12816BCC-AF9E-44A9-9AE5-C246AF2A0587} {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} + {9C7106CA-96B8-4ABE-B3B4-9357AB8ACB41} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} + {DADEA836-5CC3-474C-A2D8-4D0F31118CD7} = {32A630A7-2598-41D7-B625-204CD906F5FB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/MQTTnet.sln b/MQTTnet.sln index be6a0b6f..28d5dcab 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2010 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Core.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Core.Tests.csproj", "{A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Tests\MQTTnet.Core.Tests\MQTTnet.Tests.csproj", "{A7FF0C91-25DE-4BA6-B39E-F54E8DADF1CC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9248C2E1-B9D6-40BF-81EC-86004D7765B4}" EndProject @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{67C28AC1 Build\MQTTnet.AspNetCore.nuspec = Build\MQTTnet.AspNetCore.nuspec Build\MQTTnet.Extensions.ManagedClient.nuspec = Build\MQTTnet.Extensions.ManagedClient.nuspec Build\MQTTnet.Extensions.Rpc.nuspec = Build\MQTTnet.Extensions.Rpc.nuspec + Build\MQTTnet.NETStandard.nuspec = Build\MQTTnet.NETStandard.nuspec Build\MQTTnet.nuspec = Build\MQTTnet.nuspec Build\upload.ps1 = Build\upload.ps1 EndProjectSection @@ -47,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Tests\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Server", "Source\MQTTnet.Server\MQTTnet.Server.csproj", "{5699FB8C-838C-4AB0-80A5-9CA809F9B65B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -226,6 +229,22 @@ Global {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x64.Build.0 = Release|Any CPU {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.ActiveCfg = Release|Any CPU {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E}.Release|x86.Build.0 = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|ARM.Build.0 = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|x64.Build.0 = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Debug|x86.Build.0 = Debug|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|Any CPU.Build.0 = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|ARM.ActiveCfg = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|ARM.Build.0 = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|x64.ActiveCfg = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|x64.Build.0 = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|x86.ActiveCfg = Release|Any CPU + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,6 +260,7 @@ Global {998D04DD-7CB0-45F5-A393-E2495C16399E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} {C400533A-8EBA-4F0B-BF4D-295C3708604B} = {12816BCC-AF9E-44A9-9AE5-C246AF2A0587} {61B62223-F5D0-48E4-BBD6-2CBA9353CB5E} = {9248C2E1-B9D6-40BF-81EC-86004D7765B4} + {5699FB8C-838C-4AB0-80A5-9CA809F9B65B} = {32A630A7-2598-41D7-B625-204CD906F5FB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/MQTTnet.sln.DotSettings b/MQTTnet.sln.DotSettings index 023e3d60..20d5d147 100644 --- a/MQTTnet.sln.DotSettings +++ b/MQTTnet.sln.DotSettings @@ -7,6 +7,7 @@ True True True + True True True True diff --git a/README.md b/README.md index 24eb1e78..b93dd2c0 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,14 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * Async support * TLS 1.2 support for client and server (but not UWP servers) -* Extensible communication channels (i.e. In-Memory, TCP, TCP+TLS, WS) +* Extensible communication channels (e.g. In-Memory, TCP, TCP+TLS, WS) * Lightweight (only the low level implementation of MQTT, no overhead) * Performance optimized (processing ~70.000 messages / second)* +* Uniform API across all supported versions of the MQTT protocol * Interfaces included for mocking and testing * Access to internal trace messages -* Unit tested (~100 tests) +* Unit tested (~200 tests) +* No external dependencies \* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_. @@ -48,6 +50,17 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * WebSockets supported (via ASP.NET Core 2.0, separate nuget) * A custom message interceptor can be added which allows transforming or extending every received application message * Validate subscriptions and deny subscribing of certain topics depending on requesting clients +* Connect clients with different protocol versions at the same time. + +## MQTTnet Server + +_MQTTnet Server_ is a standalone cross platform MQTT server (like mosquitto) basing on this library. It has the following features. +* Running portable (no installation required) +* Runs und Windows, Linux, macOS, Raspberry Pi +* Python scripting support for manipulating messages, validation of clients, building business logic etc. +* Supports WebSocket and TCP (with and without TLS) connections +* Provides a HTTP based API (including Swagger endpoint) +* Extensive configuration parameters and customization supported ## Supported frameworks @@ -62,7 +75,7 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov ## Supported MQTT versions -* 5.0.0 (planned) +* 5.0.0 * 3.1.1 * 3.1.0 @@ -93,9 +106,11 @@ This library is used in the following projects: If you use this library and want to see your project here please create a pull request. -## MIT License +## License -Copyright (c) 2017-2018 Christian Kratky +MIT License + +MQTTnet Copyright (c) 2016-2019 Christian Kratky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs index 2125c7df..94fc8e41 100644 --- a/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs @@ -28,10 +28,9 @@ namespace MQTTnet.AspNetCore } var adapter = app.ApplicationServices.GetRequiredService(); - using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol)) + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false)) { - var endpoint = $"{context.Connection.RemoteIpAddress}:{context.Connection.RemotePort}"; - await adapter.RunWebSocketConnectionAsync(webSocket, endpoint); + await adapter.RunWebSocketConnectionAsync(webSocket, context); } }); diff --git a/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs index e308ccd4..e8f570c2 100644 --- a/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs +++ b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs @@ -3,8 +3,9 @@ using System.Net; using MQTTnet.Adapter; using MQTTnet.AspNetCore.Client.Tcp; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Diagnostics; -using MQTTnet.Serializer; +using MQTTnet.Formatter; namespace MQTTnet.AspNetCore.Client { @@ -14,15 +15,16 @@ namespace MQTTnet.AspNetCore.Client { if (options == null) throw new ArgumentNullException(nameof(options)); - var serializer = new MqttPacketSerializer { ProtocolVersion = options.ProtocolVersion }; - switch (options.ChannelOptions) { case MqttClientTcpOptions tcpOptions: { var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort()); var tcpConnection = new TcpConnection(endpoint); - return new MqttConnectionContext(serializer, tcpConnection); + + var writer = new SpanBasedMqttPacketWriter(); + var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, writer); + return new MqttConnectionContext(formatter, tcpConnection); } default: { diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs index 55192d6c..f5bb0b4c 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs @@ -5,6 +5,10 @@ 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 diff --git a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj index dc477f76..86db7495 100644 --- a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj +++ b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj @@ -1,12 +1,13 @@  - netstandard2.0 + netstandard2.0;netcoreapp2.1 7.2 + true @@ -15,6 +16,7 @@ + diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs index a5e567a7..6febcba5 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -1,9 +1,10 @@ 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 MQTTnet.Serializer; using System; using System.IO.Pipelines; using System.Threading; @@ -13,35 +14,68 @@ namespace MQTTnet.AspNetCore { public class MqttConnectionContext : IMqttChannelAdapter { - public MqttConnectionContext( - IMqttPacketSerializer packetSerializer, - ConnectionContext connection) + public MqttConnectionContext(MqttPacketFormatterAdapter packetFormatterAdapter, ConnectionContext connection) { - PacketSerializer = packetSerializer; - Connection = 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; + } + + _reader = new SpanBasedMqttPacketBodyReader(); } - public string Endpoint => Connection.ConnectionId; + private PipeReader _input; + private PipeWriter _output; + private readonly SpanBasedMqttPacketBodyReader _reader; + + public string Endpoint + { + get + { + var connection = Http?.HttpContext?.Connection; + if (connection == null) + { + return Connection.ConnectionId; + } + + return $"{connection.RemoteIpAddress}:{connection.RemotePort}"; + } + } + + public bool IsSecureConnection => Http?.HttpContext?.Request?.IsHttps ?? false; + + private IHttpContextFeature Http => Connection.Features.Get(); + public ConnectionContext Connection { get; } - public IMqttPacketSerializer PacketSerializer { get; } - public event EventHandler ReadingPacketStarted; - public event EventHandler ReadingPacketCompleted; + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + + public long BytesSent { get; set; } + public long BytesReceived { get; set; } + + public Action ReadingPacketStartedCallback { get; set; } + public Action ReadingPacketCompletedCallback { get; set; } private readonly SemaphoreSlim _writerSemaphore = new SemaphoreSlim(1, 1); - public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + public async Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { if (Connection is TcpConnection tcp && !tcp.IsConnected) { - return tcp.StartAsync(); + await tcp.StartAsync().ConfigureAwait(false); } - return Task.CompletedTask; + + _input = Connection.Transport.Input; + _output = Connection.Transport.Output; } public Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { - Connection.Transport.Input.Complete(); - Connection.Transport.Output.Complete(); + _input?.Complete(); + _output?.Complete(); return Task.CompletedTask; } @@ -74,14 +108,15 @@ namespace MQTTnet.AspNetCore { if (!buffer.IsEmpty) { - if (PacketSerializer.TryDeserialize(buffer, out var packet, out consumed, out observed)) + if (PacketFormatterAdapter.TryDecode(_reader, 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 - ReadingPacketStarted?.Invoke(this, EventArgs.Empty); + ReadingPacketStartedCallback?.Invoke(); } } else if (readResult.IsCompleted) @@ -100,22 +135,29 @@ namespace MQTTnet.AspNetCore } finally { - ReadingPacketCompleted?.Invoke(this, EventArgs.Empty); + ReadingPacketCompletedCallback?.Invoke(); } cancellationToken.ThrowIfCancellationRequested(); return null; } - public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public async Task SendPacketAsync(MqttBasePacket packet, TimeSpan timeout, CancellationToken cancellationToken) { - var buffer = PacketSerializer.Serialize(packet).AsMemory(); - var output = Connection.Transport.Output; + var formatter = PacketFormatterAdapter; + await _writerSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - await output.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + var buffer = formatter.Encode(packet); + var msg = buffer.AsMemory(); + var output = _output; + msg.CopyTo(output.GetMemory(msg.Length)); + BytesSent += msg.Length; + PacketFormatterAdapter.FreeBuffer(); + output.Advance(msg.Length); + await output.FlushAsync().ConfigureAwait(false); } finally { diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs index 23903894..8cc1022d 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs @@ -1,16 +1,16 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using MQTTnet.Adapter; -using MQTTnet.Serializer; using MQTTnet.Server; using System; using System.Threading.Tasks; +using MQTTnet.Formatter; namespace MQTTnet.AspNetCore { public class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter - { - public event EventHandler ClientAccepted; + { + public Func ClientHandler { get; set; } public override async Task OnConnectedAsync(ConnectionContext connection) { @@ -21,14 +21,15 @@ namespace MQTTnet.AspNetCore transferFormatFeature.ActiveFormat = TransferFormat.Binary; } - - var serializer = new MqttPacketSerializer(); - using (var adapter = new MqttConnectionContext(serializer, connection)) + var writer = new SpanBasedMqttPacketWriter(); + var formatter = new MqttPacketFormatterAdapter(writer); + using (var adapter = new MqttConnectionContext(formatter, connection)) { - var args = new MqttServerAdapterClientAcceptedEventArgs(adapter); - ClientAccepted?.Invoke(this, args); - - await args.SessionTask; + var clientHandler = ClientHandler; + if (clientHandler != null) + { + await clientHandler(adapter).ConfigureAwait(false); + } } } @@ -41,7 +42,7 @@ namespace MQTTnet.AspNetCore { return Task.CompletedTask; } - + public void Dispose() { } diff --git a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index dbd93604..455840cd 100644 --- a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -1,17 +1,27 @@ using System; using System.Net.WebSockets; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using MQTTnet.Adapter; using MQTTnet.Diagnostics; +using MQTTnet.Formatter; using MQTTnet.Implementations; -using MQTTnet.Serializer; using MQTTnet.Server; namespace MQTTnet.AspNetCore { public class MqttWebSocketServerAdapter : IMqttServerAdapter { - public event EventHandler ClientAccepted; + private readonly IMqttNetChildLogger _logger; + + public MqttWebSocketServerAdapter(IMqttNetChildLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + _logger = logger.CreateChildLogger(nameof(MqttTcpServerAdapter)); + } + + public Func ClientHandler { get; set; } public Task StartAsync(IMqttServerOptions options) { @@ -23,24 +33,31 @@ namespace MQTTnet.AspNetCore return Task.CompletedTask; } - public async Task RunWebSocketConnectionAsync(WebSocket webSocket, string endpoint) + public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext) { if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); - var clientAdapter = new MqttChannelAdapter(new MqttWebSocketChannel(webSocket, endpoint), new MqttPacketSerializer(), new MqttNetLogger().CreateChildLogger(nameof(MqttWebSocketServerAdapter))); + var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}"; + + var clientCertificate = await httpContext.Connection.GetClientCertificateAsync().ConfigureAwait(false); + var isSecureConnection = clientCertificate != null; + clientCertificate?.Dispose(); - var eventArgs = new MqttServerAdapterClientAcceptedEventArgs(clientAdapter); - ClientAccepted?.Invoke(this, eventArgs); - - if (eventArgs.SessionTask != null) + var clientHandler = ClientHandler; + if (clientHandler != null) { - await eventArgs.SessionTask.ConfigureAwait(false); + var writer = new SpanBasedMqttPacketWriter(); + var formatter = new MqttPacketFormatterAdapter(writer); + var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection); + using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger.CreateChildLogger(nameof(MqttWebSocketServerAdapter)))) + { + await clientHandler(channelAdapter).ConfigureAwait(false); + } } } - + public void Dispose() { - StopAsync().GetAwaiter().GetResult(); } } } \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/ReaderExtensions.cs b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs index 6c770b9e..2746c7db 100644 --- a/Source/MQTTnet.AspnetCore/ReaderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs @@ -2,26 +2,36 @@ using System.Buffers; using MQTTnet.Adapter; using MQTTnet.Exceptions; +using MQTTnet.Formatter; using MQTTnet.Packets; -using MQTTnet.Serializer; namespace MQTTnet.AspNetCore { public static class ReaderExtensions { - public static bool TryDeserialize(this IMqttPacketSerializer serializer, in ReadOnlySequence input, out MqttBasePacket packet, out SequencePosition consumed, out SequencePosition observed) + public static bool TryDecode(this MqttPacketFormatterAdapter formatter, + SpanBasedMqttPacketBodyReader reader, + in ReadOnlySequence input, + out MqttBasePacket 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 var bodyLength)) + if (!TryReadBodyLength(ref copy, out int headerLength, out var bodyLength)) { return false; } @@ -32,34 +42,45 @@ namespace MQTTnet.AspNetCore } var bodySlice = copy.Slice(0, bodyLength); - var buffer = bodySlice.GetArray(); - packet = serializer.Deserialize(new ReceivedMqttPacket(fixedheader, new MqttPacketBodyReader(buffer, 0, buffer.Length))); + var buffer = bodySlice.GetMemory(); + reader.SetBuffer(buffer); + + var receivedMqttPacket = new ReceivedMqttPacket(fixedheader, reader, 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; } - private static byte[] GetArray(this in ReadOnlySequence input) + private static ReadOnlyMemory GetMemory(this in ReadOnlySequence input) { if (input.IsSingleSegment) { - return input.First.Span.ToArray(); + return input.First; } // Should be rare return input.ToArray(); } - private static bool TryReadBodyLength(ref ReadOnlySequence input, out int result) + private 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; - result = 0; - - var temp = input.Slice(0, Math.Min(5, input.Length)).GetArray(); + headerLength = 0; + bodyLength = 0; + + var temp = input.Slice(0, Math.Min(5, input.Length)).GetMemory(); do { @@ -67,13 +88,13 @@ namespace MQTTnet.AspNetCore { return false; } - encodedByte = temp[index]; + encodedByte = temp.Span[index]; index++; value += (byte)(encodedByte & 127) * multiplier; if (multiplier > 128 * 128 * 128) { - throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.AsSpan(1, index).ToArray())})."); + throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.Slice(1, index).ToArray())})."); } multiplier *= 128; @@ -81,7 +102,8 @@ namespace MQTTnet.AspNetCore input = input.Slice(index); - result = value; + headerLength = index; + bodyLength = value; return true; } } diff --git a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs new file mode 100644 index 00000000..2b118f7b --- /dev/null +++ b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs @@ -0,0 +1,124 @@ +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using System; +using System.Buffers.Binary; +using System.Text; + +namespace MQTTnet.AspNetCore +{ + public class SpanBasedMqttPacketBodyReader : IMqttPacketBodyReader + { + private ReadOnlyMemory _buffer; + private int _offset; + + public void SetBuffer(ReadOnlyMemory buffer) + { + _buffer = buffer; + _offset = 0; + } + + public ulong Length => (ulong)_buffer.Length; + + public bool EndOfStream => _buffer.Length.Equals(_offset); + + public ulong Offset => (ulong)_offset; + + public byte ReadByte() + { + return _buffer.Span[_offset++]; + } + + public byte[] ReadRemainingData() + { + return _buffer.Slice(_offset).ToArray(); + } + + public byte[] ReadWithLengthPrefix() + { + return ReadSegmentWithLengthPrefix().ToArray(); + } + + private ReadOnlySpan ReadSegmentWithLengthPrefix() + { + var span = _buffer.Span; + var length = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(_offset)); + + var result = span.Slice(_offset+2, length); + _offset += 2 + length; + return result; + } + + public unsafe string ReadStringWithLengthPrefix() + { + var buffer = ReadSegmentWithLengthPrefix(); + if (buffer.Length == 0) + { + return string.Empty; + } + + fixed (byte* bytes = &buffer.GetPinnableReference()) + { + var result = Encoding.UTF8.GetString(bytes, buffer.Length); + return result; + } + } + + public ushort ReadTwoByteInteger() + { + var result = BinaryPrimitives.ReadUInt16BigEndian(_buffer.Span.Slice(_offset)); + _offset += 2; + return result; + } + + public uint ReadFourByteInteger() + { + var result = BinaryPrimitives.ReadUInt32BigEndian(_buffer.Span.Slice(_offset)); + _offset += 4; + return result; + } + + public uint ReadVariableLengthInteger() + { + var multiplier = 1; + var value = 0U; + byte encodedByte; + + do + { + encodedByte = ReadByte(); + value += (uint)((encodedByte & 127) * multiplier); + + if (multiplier > 2097152) + { + throw new MqttProtocolViolationException("Variable length integer is invalid."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + return value; + } + + public bool ReadBoolean() + { + var buffer = ReadByte(); + + if (buffer == 0) + { + return false; + } + + if (buffer == 1) + { + return true; + } + + throw new MqttProtocolViolationException("Boolean values can be 0 or 1 only."); + } + + public void Seek(ulong position) + { + _offset = (int)position; + } + } +} diff --git a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs new file mode 100644 index 00000000..27e13c49 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs @@ -0,0 +1,146 @@ +using MQTTnet.Formatter; +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Text; + +namespace MQTTnet.AspNetCore +{ + public class SpanBasedMqttPacketWriter : IMqttPacketWriter + { + private readonly ArrayPool _pool; + + public SpanBasedMqttPacketWriter() + { + _pool = ArrayPool.Create(); + + } + + private byte[] _buffer; + private int _position; + + public int Length { get; set; } + + public void FreeBuffer() + { + _pool.Return(_buffer); + } + + public byte[] GetBuffer() + { + return _buffer; + } + + public void Reset(int v) + { + _buffer = _pool.Rent(1500); + Length = v; + _position = v; + } + + public void Seek(int v) + { + _position = v; + } + + public void Write(byte value) + { + GrowIfNeeded(1); + _buffer[_position] = value; + Commit(1); + } + + public void Write(ushort value) + { + GrowIfNeeded(2); + + BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_position), value); + Commit(2); + } + + public void Write(IMqttPacketWriter propertyWriter) + { + if (propertyWriter == null) throw new ArgumentNullException(nameof(propertyWriter)); + + GrowIfNeeded(propertyWriter.Length); + Write(propertyWriter.GetBuffer(), 0, propertyWriter.Length); + Commit(propertyWriter.Length); + } + + public void Write(byte[] payload, int start, int length) + { + GrowIfNeeded(length); + + payload.AsSpan(start, length).CopyTo(_buffer.AsSpan(_position)); + Commit(length); + } + + public void WriteVariableLengthInteger(uint value) + { + GrowIfNeeded(4); + + var x = value; + do + { + var encodedByte = x % 128; + x = x / 128; + if (x > 0) + { + encodedByte = encodedByte | 128; + } + + _buffer[_position] = (byte)encodedByte; + Commit(1); + } while (x > 0); + } + + public void WriteWithLengthPrefix(string value) + { + var bytesLength = Encoding.UTF8.GetByteCount(value ?? string.Empty); + GrowIfNeeded(bytesLength + 2); + + Write((ushort)bytesLength); + Encoding.UTF8.GetBytes(value ?? string.Empty, 0, value?.Length ?? 0, _buffer, _position); + Commit(bytesLength); + } + + public void WriteWithLengthPrefix(byte[] payload) + { + GrowIfNeeded(payload.Length + 2); + + Write((ushort)payload.Length); + payload.CopyTo(_buffer, _position); + Commit(payload.Length); + } + + private void Commit(int count) + { + if (_position == Length) + { + Length += count; + } + + _position += count; + } + + private void GrowIfNeeded(int requiredAdditional) + { + var requiredTotal = _position + requiredAdditional; + if (_buffer.Length >= requiredTotal) + { + return; + } + + var newBufferLength = _buffer.Length; + while (newBufferLength < requiredTotal) + { + newBufferLength *= 2; + } + + var newBuffer = _pool.Rent(newBufferLength); + Array.Copy(_buffer, newBuffer, _buffer.Length); + _pool.Return(_buffer); + _buffer = newBuffer; + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs new file mode 100644 index 00000000..e976722e --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ApplicationMessageProcessedHandlerDelegate : IApplicationMessageProcessedHandler + { + private readonly Func _handler; + + public ApplicationMessageProcessedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = context => + { + handler(context); + return Task.FromResult(0); + }; + } + + public ApplicationMessageProcessedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleApplicationMessageProcessedAsync(ApplicationMessageProcessedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs new file mode 100644 index 00000000..978f30be --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ApplicationMessageSkippedHandlerDelegate : IApplicationMessageSkippedHandler + { + private readonly Func _handler; + + public ApplicationMessageSkippedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public ApplicationMessageSkippedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleApplicationMessageSkippedAsync(ApplicationMessageSkippedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs new file mode 100644 index 00000000..85a796b9 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ConnectingFailedHandlerDelegate : IConnectingFailedHandler + { + private readonly Func _handler; + + public ConnectingFailedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public ConnectingFailedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleConnectingFailedAsync(ManagedProcessFailedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs new file mode 100644 index 00000000..08ed197a --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public interface IApplicationMessageProcessedHandler + { + Task HandleApplicationMessageProcessedAsync(ApplicationMessageProcessedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs new file mode 100644 index 00000000..e664cd2f --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public interface IApplicationMessageSkippedHandler + { + Task HandleApplicationMessageSkippedAsync(ApplicationMessageSkippedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs new file mode 100644 index 00000000..aabdbfb0 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public interface IConnectingFailedHandler + { + Task HandleConnectingFailedAsync(ManagedProcessFailedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs index b3835e3e..9919081f 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; namespace MQTTnet.Extensions.ManagedClient { @@ -12,14 +13,14 @@ namespace MQTTnet.Extensions.ManagedClient int PendingApplicationMessagesCount { get; } IManagedMqttClientOptions Options { get; } - event EventHandler Connected; - event EventHandler Disconnected; + IMqttClientConnectedHandler ConnectedHandler { get; set; } + IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } - event EventHandler ApplicationMessageProcessed; - event EventHandler ApplicationMessageSkipped; + IApplicationMessageProcessedHandler ApplicationMessageProcessedHandler { get; set; } + IApplicationMessageSkippedHandler ApplicationMessageSkippedHandler { get; set; } - event EventHandler ConnectingFailed; - event EventHandler SynchronizingSubscriptionsFailed; + IConnectingFailedHandler ConnectingFailedHandler { get; set; } + ISynchronizingSubscriptionsFailedHandler SynchronizingSubscriptionsFailedHandler { get; set; } Task StartAsync(IManagedMqttClientOptions options); Task StopAsync(); diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs index f59aca7c..a705efea 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs @@ -1,5 +1,5 @@ using System; -using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs new file mode 100644 index 00000000..f76f18fb --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public interface ISynchronizingSubscriptionsFailedHandler + { + Task HandleSynchronizingSubscriptionsFailedAsync(ManagedProcessFailedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index 866a75da..59f1c9a9 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -4,13 +4,15 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; -using MqttClientConnectedEventArgs = MQTTnet.Client.MqttClientConnectedEventArgs; -using MqttClientDisconnectedEventArgs = MQTTnet.Client.MqttClientDisconnectedEventArgs; namespace MQTTnet.Extensions.ManagedClient { @@ -25,21 +27,18 @@ namespace MQTTnet.Extensions.ManagedClient private CancellationTokenSource _connectionCancellationToken; private CancellationTokenSource _publishingCancellationToken; + private Task _maintainConnectionTask; private ManagedMqttClientStorageManager _storageManager; - + + private bool _disposed = false; private bool _subscriptionsNotPushed; public ManagedMqttClient(IMqttClient mqttClient, IMqttNetChildLogger logger) { - if (logger == null) throw new ArgumentNullException(nameof(logger)); - _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); - _mqttClient.Connected += OnConnected; - _mqttClient.Disconnected += OnDisconnected; - _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; - + if (logger == null) throw new ArgumentNullException(nameof(logger)); _logger = logger.CreateChildLogger(nameof(ManagedMqttClient)); } @@ -48,18 +47,36 @@ namespace MQTTnet.Extensions.ManagedClient public int PendingApplicationMessagesCount => _messageQueue.Count; public IManagedMqttClientOptions Options { get; private set; } - public event EventHandler Connected; - public event EventHandler Disconnected; + public IMqttClientConnectedHandler ConnectedHandler + { + get => _mqttClient.ConnectedHandler; + set => _mqttClient.ConnectedHandler = value; + } - public event EventHandler ApplicationMessageReceived; - public event EventHandler ApplicationMessageProcessed; - public event EventHandler ApplicationMessageSkipped; + public IMqttClientDisconnectedHandler DisconnectedHandler + { + get => _mqttClient.DisconnectedHandler; + set => _mqttClient.DisconnectedHandler = value; + } - public event EventHandler ConnectingFailed; - public event EventHandler SynchronizingSubscriptionsFailed; + public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler + { + get => _mqttClient.ApplicationMessageReceivedHandler; + set => _mqttClient.ApplicationMessageReceivedHandler = value; + } + + public IApplicationMessageProcessedHandler ApplicationMessageProcessedHandler { get; set; } + + public IApplicationMessageSkippedHandler ApplicationMessageSkippedHandler { get; set; } + + public IConnectingFailedHandler ConnectingFailedHandler { get; set; } + + public ISynchronizingSubscriptionsFailedHandler SynchronizingSubscriptionsFailedHandler { get; set; } public async Task StartAsync(IManagedMqttClientOptions options) { + ThrowIfDisposed(); + if (options == null) throw new ArgumentNullException(nameof(options)); if (options.ClientOptions == null) throw new ArgumentException("The client options are not set.", nameof(options)); @@ -68,7 +85,7 @@ namespace MQTTnet.Extensions.ManagedClient throw new NotSupportedException("The managed client does not support existing sessions."); } - if (_connectionCancellationToken != null) throw new InvalidOperationException("The managed client is already started."); + if (!_maintainConnectionTask?.IsCompleted ?? false) throw new InvalidOperationException("The managed client is already started."); Options = options; @@ -86,56 +103,86 @@ namespace MQTTnet.Extensions.ManagedClient _connectionCancellationToken = new CancellationTokenSource(); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Run(() => MaintainConnectionAsync(_connectionCancellationToken.Token), _connectionCancellationToken.Token); + _maintainConnectionTask = Task.Run(() => MaintainConnectionAsync(_connectionCancellationToken.Token), _connectionCancellationToken.Token); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed _logger.Info("Started"); } - public Task StopAsync() + public async Task StopAsync() { + ThrowIfDisposed(); + StopPublishing(); StopMaintainingConnection(); _messageQueue.Clear(); - return Task.FromResult(0); + if (_maintainConnectionTask != null) + { + await Task.WhenAny(_maintainConnectionTask); + _maintainConnectionTask = null; + } } - public Task PublishAsync(MqttApplicationMessage applicationMessage) + public async Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) { + ThrowIfDisposed(); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - return PublishAsync(new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage).Build()); + await PublishAsync(new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage).Build()).ConfigureAwait(false); + return new MqttClientPublishResult(); } public async Task PublishAsync(ManagedMqttApplicationMessage applicationMessage) { + ThrowIfDisposed(); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage.Topic); + ManagedMqttApplicationMessage removedMessage = null; - lock (_messageQueue) + ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null; + + try { - if (_messageQueue.Count >= Options.MaxPendingMessages) + lock (_messageQueue) { - if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + if (_messageQueue.Count >= Options.MaxPendingMessages) { - _logger.Verbose("Skipping publish of new application message because internal queue is full."); - ApplicationMessageSkipped?.Invoke(this, new ApplicationMessageSkippedEventArgs(applicationMessage)); - return; + if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + { + _logger.Verbose("Skipping publish of new application message because internal queue is full."); + applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(applicationMessage); + return; + } + + if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + { + removedMessage = _messageQueue.RemoveFirst(); + _logger.Verbose("Removed oldest application message from internal queue because it is full."); + applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(removedMessage); + } } - if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + _messageQueue.Enqueue(applicationMessage); + } + } + finally + { + if (applicationMessageSkippedEventArgs != null) + { + var applicationMessageSkippedHandler = ApplicationMessageSkippedHandler; + if (applicationMessageSkippedHandler != null) { - removedMessage = _messageQueue.RemoveFirst(); - _logger.Verbose("Removed oldest application message from internal queue because it is full."); - ApplicationMessageSkipped?.Invoke(this, new ApplicationMessageSkippedEventArgs(removedMessage)); + await applicationMessageSkippedHandler.HandleApplicationMessageSkippedAsync(applicationMessageSkippedEventArgs).ConfigureAwait(false); } } - _messageQueue.Enqueue(applicationMessage); } - + if (_storageManager != null) { if (removedMessage != null) @@ -149,6 +196,8 @@ namespace MQTTnet.Extensions.ManagedClient public Task SubscribeAsync(IEnumerable topicFilters) { + ThrowIfDisposed(); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); lock (_subscriptions) @@ -165,6 +214,8 @@ namespace MQTTnet.Extensions.ManagedClient public Task UnsubscribeAsync(IEnumerable topics) { + ThrowIfDisposed(); + if (topics == null) throw new ArgumentNullException(nameof(topics)); lock (_subscriptions) @@ -184,8 +235,31 @@ namespace MQTTnet.Extensions.ManagedClient public void Dispose() { - _connectionCancellationToken?.Dispose(); - _publishingCancellationToken?.Dispose(); + if (_disposed) + { + return; + } + + _disposed = true; + + StopPublishing(); + StopMaintainingConnection(); + + if (_maintainConnectionTask != null) + { + Task.WaitAny(_maintainConnectionTask); + _maintainConnectionTask = null; + } + + _mqttClient.Dispose(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ManagedMqttClient)); + } } private async Task MaintainConnectionAsync(CancellationToken cancellationToken) @@ -206,8 +280,19 @@ namespace MQTTnet.Extensions.ManagedClient } finally { - await _mqttClient.DisconnectAsync().ConfigureAwait(false); - _logger.Info("Stopped"); + if (!_disposed) + { + try + { + await _mqttClient.DisconnectAsync().ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while disconnecting."); + } + + _logger.Info("Stopped"); + } } } @@ -332,20 +417,20 @@ namespace MQTTnet.Extensions.ManagedClient } finally { - ApplicationMessageProcessed?.Invoke(this, new ApplicationMessageProcessedEventArgs(message, transmitException)); + ApplicationMessageProcessedHandler?.HandleApplicationMessageProcessedAsync(new ApplicationMessageProcessedEventArgs(message, transmitException)).GetAwaiter().GetResult(); } } private async Task SynchronizeSubscriptionsAsync() { - _logger.Info(nameof(ManagedMqttClient), "Synchronizing subscriptions"); + _logger.Info("Synchronizing subscriptions"); List subscriptions; HashSet unsubscriptions; lock (_subscriptions) { - subscriptions = _subscriptions.Select(i => new TopicFilter(i.Key, i.Value)).ToList(); + subscriptions = _subscriptions.Select(i => new TopicFilter { Topic = i.Key, QualityOfServiceLevel = i.Value }).ToList(); unsubscriptions = new HashSet(_unsubscriptions); _unsubscriptions.Clear(); @@ -362,12 +447,12 @@ namespace MQTTnet.Extensions.ManagedClient { if (unsubscriptions.Any()) { - await _mqttClient.UnsubscribeAsync(unsubscriptions).ConfigureAwait(false); + await _mqttClient.UnsubscribeAsync(unsubscriptions.ToArray()).ConfigureAwait(false); } if (subscriptions.Any()) { - await _mqttClient.SubscribeAsync(subscriptions).ConfigureAwait(false); + await _mqttClient.SubscribeAsync(subscriptions.ToArray()).ConfigureAwait(false); } } catch (Exception exception) @@ -375,7 +460,11 @@ namespace MQTTnet.Extensions.ManagedClient _logger.Warning(exception, "Synchronizing subscriptions failed."); _subscriptionsNotPushed = true; - SynchronizingSubscriptionsFailed?.Invoke(this, new MqttManagedProcessFailedEventArgs(exception)); + var synchronizingSubscriptionsFailedHandler = SynchronizingSubscriptionsFailedHandler; + if (SynchronizingSubscriptionsFailedHandler != null) + { + await synchronizingSubscriptionsFailedHandler.HandleSynchronizingSubscriptionsFailedAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); + } } } @@ -393,26 +482,16 @@ namespace MQTTnet.Extensions.ManagedClient } catch (Exception exception) { - ConnectingFailed?.Invoke(this, new MqttManagedProcessFailedEventArgs(exception)); + var connectingFailedHandler = ConnectingFailedHandler; + if (connectingFailedHandler != null) + { + await connectingFailedHandler.HandleConnectingFailedAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); + } + return ReconnectionResult.NotConnected; } } - - private void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs) - { - ApplicationMessageReceived?.Invoke(this, eventArgs); - } - - private void OnDisconnected(object sender, MqttClientDisconnectedEventArgs eventArgs) - { - Disconnected?.Invoke(this, eventArgs); - } - - private void OnConnected(object sender, MqttClientConnectedEventArgs eventArgs) - { - Connected?.Invoke(this, eventArgs); - } - + private void StartPublishing() { if (_publishingCancellationToken != null) @@ -440,4 +519,4 @@ namespace MQTTnet.Extensions.ManagedClient _connectionCancellationToken = null; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs index ba5a16f7..b6387918 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs @@ -1,39 +1,229 @@ using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; using MQTTnet.Protocol; namespace MQTTnet.Extensions.ManagedClient { public static class ManagedMqttClientExtensions { - public static Task SubscribeAsync(this IManagedMqttClient managedClient, params TopicFilter[] topicFilters) + public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, Func handler) { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (client == null) throw new ArgumentNullException(nameof(client)); - return managedClient.SubscribeAsync(topicFilters); + if (handler == null) + { + return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + } + + return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); } - public static Task SubscribeAsync(this IManagedMqttClient managedClient, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, Action handler) { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + } + + return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); + } + + public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, IMqttClientConnectedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.ConnectedHandler = handler; + return client; + } + + public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, Func handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + } + + return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); + } + + public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, Action handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + } + + return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); + } + + public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, IMqttClientDisconnectedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.DisconnectedHandler = handler; + return client; + } + + public static IManagedMqttClient UseApplicationMessageReceivedHandler(this IManagedMqttClient client, Func handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IManagedMqttClient UseApplicationMessageReceivedHandler(this IManagedMqttClient client, Action handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IManagedMqttClient UseApplicationMessageReceivedHandler(this IManagedMqttClient client, IMqttApplicationMessageReceivedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.ApplicationMessageReceivedHandler = handler; + return client; + } + + public static Task SubscribeAsync(this IManagedMqttClient client, params TopicFilter[] topicFilters) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + return client.SubscribeAsync(topicFilters); + } + + public static Task SubscribeAsync(this IManagedMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (client == null) throw new ArgumentNullException(nameof(client)); if (topic == null) throw new ArgumentNullException(nameof(topic)); - return managedClient.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); } - public static Task SubscribeAsync(this IManagedMqttClient managedClient, string topic) + public static Task SubscribeAsync(this IManagedMqttClient client, string topic) { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (client == null) throw new ArgumentNullException(nameof(client)); if (topic == null) throw new ArgumentNullException(nameof(topic)); - return managedClient.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); + return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); } - public static Task UnsubscribeAsync(this IManagedMqttClient managedClient, params string[] topicFilters) + public static Task UnsubscribeAsync(this IManagedMqttClient client, params string[] topicFilters) { - if (managedClient == null) throw new ArgumentNullException(nameof(managedClient)); + if (client == null) throw new ArgumentNullException(nameof(client)); - return managedClient.UnsubscribeAsync(topicFilters); + return client.UnsubscribeAsync(topicFilters); + } + + public static async Task PublishAsync(this IManagedMqttClient client, IEnumerable applicationMessages) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await client.PublishAsync(applicationMessage).ConfigureAwait(false); + } + } + + public static Task PublishAsync(this IManagedMqttClient client, MqttApplicationMessage applicationMessage) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + return client.PublishAsync(applicationMessage, CancellationToken.None); + } + + public static async Task PublishAsync(this IManagedMqttClient client, params MqttApplicationMessage[] applicationMessages) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await client.PublishAsync(applicationMessage, CancellationToken.None).ConfigureAwait(false); + } + } + + public static Task PublishAsync(this IManagedMqttClient client, string topic) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(builder => builder + .WithTopic(topic)); + } + + public static Task PublishAsync(this IManagedMqttClient client, string topic, string payload) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload)); + } + + public static Task PublishAsync(this IManagedMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel)); + } + + public static Task PublishAsync(this IManagedMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .WithRetainFlag(retain)); + } + + public static Task PublishAsync(this IManagedMqttClient client, Func builder, CancellationToken cancellationToken) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + var message = builder(new MqttApplicationMessageBuilder()).Build(); + return client.PublishAsync(message, cancellationToken); + } + + public static Task PublishAsync(this IManagedMqttClient client, Func builder) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + var message = builder(new MqttApplicationMessageBuilder()).Build(); + return client.PublishAsync(message, CancellationToken.None); } } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs index cba801bb..537e1bb9 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs @@ -1,5 +1,6 @@ using System; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs index 3ea5b050..f58453ce 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs @@ -1,5 +1,6 @@ using System; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs index 821a7d92..772f102e 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs @@ -30,7 +30,7 @@ namespace MQTTnet.Extensions.ManagedClient { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _messagesLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) { _messages.Add(applicationMessage); await SaveAsync().ConfigureAwait(false); @@ -41,7 +41,7 @@ namespace MQTTnet.Extensions.ManagedClient { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - using (await _messagesLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _messagesLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) { var index = _messages.IndexOf(applicationMessage); if (index == -1) diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs new file mode 100644 index 00000000..bb0cf683 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class ManagedProcessFailedEventArgs : EventArgs + { + public ManagedProcessFailedEventArgs(Exception exception) + { + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } + + public Exception Exception { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs index d2ad020d..37393ac2 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs @@ -1,24 +1,23 @@ using System; -using MQTTnet.Client; using MQTTnet.Diagnostics; namespace MQTTnet.Extensions.ManagedClient { public static class MqttFactoryExtensions { - public static IManagedMqttClient CreateManagedMqttClient(this IMqttClientFactory factory) + public static IManagedMqttClient CreateManagedMqttClient(this IMqttFactory factory) { if (factory == null) throw new ArgumentNullException(nameof(factory)); - return new ManagedMqttClient(factory.CreateMqttClient(), new MqttNetLogger().CreateChildLogger()); + return new ManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger.CreateChildLogger()); } - public static IManagedMqttClient CreateManagedMqttClient(this IMqttClientFactory factory, IMqttNetLogger logger) + public static IManagedMqttClient CreateManagedMqttClient(this IMqttFactory factory, IMqttNetLogger logger) { if (factory == null) throw new ArgumentNullException(nameof(factory)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - return new ManagedMqttClient(factory.CreateMqttClient(), logger.CreateChildLogger()); + return new ManagedMqttClient(factory.CreateMqttClient(logger), logger.CreateChildLogger()); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs deleted file mode 100644 index 68dc1608..00000000 --- a/Source/MQTTnet.Extensions.ManagedClient/MqttManagedProcessFailedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MQTTnet.Extensions.ManagedClient -{ - public class MqttManagedProcessFailedEventArgs : EventArgs - { - public MqttManagedProcessFailedEventArgs(Exception exception) - { - Exception = exception; - } - - public Exception Exception { get; } - } -} diff --git a/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs new file mode 100644 index 00000000..72d6e4a0 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.ManagedClient +{ + public class SynchronizingSubscriptionsFailedHandlerDelegate : ISynchronizingSubscriptionsFailedHandler + { + private readonly Func _handler; + + public SynchronizingSubscriptionsFailedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = context => + { + handler(context); + return Task.FromResult(0); + }; + } + + public SynchronizingSubscriptionsFailedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleSynchronizingSubscriptionsFailedAsync(ManagedProcessFailedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs index ebf66ec1..c025bdfa 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -13,12 +13,17 @@ namespace MQTTnet.Extensions.Rpc { private readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); private readonly IMqttClient _mqttClient; + private readonly RpcAwareApplicationMessageReceivedHandler _applicationMessageReceivedHandler; public MqttRpcClient(IMqttClient mqttClient) { _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); - _mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived; + _applicationMessageReceivedHandler = new RpcAwareApplicationMessageReceivedHandler( + mqttClient.ApplicationMessageReceivedHandler, + HandleApplicationMessageReceivedAsync); + + _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler; } public Task ExecuteAsync(TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) @@ -45,6 +50,11 @@ namespace MQTTnet.Extensions.Rpc throw new ArgumentException("The method name cannot contain /, + or #."); } + if (!(_mqttClient.ApplicationMessageReceivedHandler is RpcAwareApplicationMessageReceivedHandler)) + { + throw new InvalidOperationException("The application message received handler was modified."); + } + var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{methodName}"; var responseTopic = requestTopic + "/response"; @@ -102,26 +112,25 @@ namespace MQTTnet.Extensions.Rpc } } - private void OnApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs eventArgs) + private Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) { if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var tcs)) { - return; - } - - if (tcs.Task.IsCompleted || tcs.Task.IsCanceled) - { - return; + return Task.FromResult(0); } tcs.TrySetResult(eventArgs.ApplicationMessage.Payload); + + return Task.FromResult(0); } public void Dispose() { + _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler.OriginalHandler; + foreach (var tcs in _waitingCalls) { - tcs.Value.SetCanceled(); + tcs.Value.TrySetCanceled(); } _waitingCalls.Clear(); diff --git a/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs new file mode 100644 index 00000000..9d90fb19 --- /dev/null +++ b/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Client.Receiving; + +namespace MQTTnet.Extensions.Rpc +{ + public class RpcAwareApplicationMessageReceivedHandler : IMqttApplicationMessageReceivedHandler + { + private readonly Func _handleReceivedApplicationMessageAsync; + + public RpcAwareApplicationMessageReceivedHandler( + IMqttApplicationMessageReceivedHandler originalHandler, + Func handleReceivedApplicationMessageAsync) + { + OriginalHandler = originalHandler; + _handleReceivedApplicationMessageAsync = handleReceivedApplicationMessageAsync ?? throw new ArgumentNullException(nameof(handleReceivedApplicationMessageAsync)); + } + + public IMqttApplicationMessageReceivedHandler OriginalHandler { get; } + + public async Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + if (OriginalHandler != null) + { + await OriginalHandler.HandleApplicationMessageReceivedAsync(eventArgs).ConfigureAwait(false); + } + + await _handleReceivedApplicationMessageAsync(eventArgs).ConfigureAwait(false); + } + } +} diff --git a/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs b/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs new file mode 100644 index 00000000..7abae83f --- /dev/null +++ b/Source/MQTTnet.Server/Configuration/RetainedApplicationMessagesModel.cs @@ -0,0 +1,11 @@ +namespace MQTTnet.Server.Configuration +{ + public class RetainedApplicationMessagesModel + { + public bool Persist { get; set; } = false; + + public int WriteInterval { get; set; } = 10; + + public string Filename { get; set; } = "RetainedApplicationMessages.json"; + } +} diff --git a/Source/MQTTnet.Server/Configuration/SettingsModel.cs b/Source/MQTTnet.Server/Configuration/SettingsModel.cs new file mode 100644 index 00000000..ce5336d2 --- /dev/null +++ b/Source/MQTTnet.Server/Configuration/SettingsModel.cs @@ -0,0 +1,53 @@ +namespace MQTTnet.Server.Configuration +{ + /// + /// Main Settings Model + /// + public class SettingsModel + { + /// + /// Set default connection timeout in seconds + /// + public int CommunicationTimeout { get; set; } = 15; + + /// + /// Set 0 to disable connection backlogging + /// + public int ConnectionBacklog { get; set; } + + /// + /// Enable support for persistent sessions + /// + public bool EnablePersistentSessions { get; set; } = false; + + /// + /// Listen Settings + /// + public TcpEndPointModel TcpEndPoint { get; set; } = new TcpEndPointModel(); + + /// + /// Encryption Listen Settings + /// + public TcpEndPointModel EncryptedTcpEndPoint { get; set; } = new TcpEndPointModel(); + + /// + /// Settings for the Web Socket endpoint. + /// + public WebSocketEndPointModel WebSocketEndPoint { get; set; } = new WebSocketEndPointModel(); + + /// + /// Set limit for max pending messages per client + /// + public int MaxPendingMessagesPerClient { get; set; } = 250; + + /// + /// The settings for retained messages. + /// + public RetainedApplicationMessagesModel RetainedApplicationMessages { get; set; } = new RetainedApplicationMessagesModel(); + + /// + /// Enables or disables the MQTTnet internal logging. + /// + public bool EnableDebugLogging { get; set; } = false; + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs b/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs new file mode 100644 index 00000000..82213901 --- /dev/null +++ b/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs @@ -0,0 +1,121 @@ +using System.IO; +using System.Net; + +namespace MQTTnet.Server.Configuration +{ + /// + /// Listen Entry Settings Model + /// + public class TcpEndPointModel + { + /// + /// Path to Certificate + /// + public string CertificatePath { get; set; } + + /// + /// Enabled / Disable + /// + public bool Enabled { get; set; } = true; + + /// + /// Listen Address + /// + public string IPv4 { get; set; } + + /// + /// Listen Address + /// + public string IPv6 { get; set; } + + /// + /// Listen Port + /// + public int Port { get; set; } = 1883; + + /// + /// Read Certificate file + /// + /// + public byte[] ReadCertificate() + { + if (string.IsNullOrEmpty(CertificatePath) || string.IsNullOrWhiteSpace(CertificatePath)) + { + throw new FileNotFoundException("No path set"); + } + + if (!File.Exists(CertificatePath)) + { + throw new FileNotFoundException($"Could not find Certificate in path: {CertificatePath}"); + } + + return File.ReadAllBytes(CertificatePath); + } + + /// + /// Read IPv4 + /// + /// + public bool TryReadIPv4(out IPAddress address) + { + if (IPv4 == "*") + { + address = IPAddress.Any; + return true; + } + + if (IPv4 == "localhost") + { + address = IPAddress.Loopback; + return true; + } + + if (IPv4 == "disable") + { + address = IPAddress.None; + return true; + } + + if (IPAddress.TryParse(IPv4, out var ip)) + { + address = ip; + return true; + } + + throw new System.Exception($"Could not parse IPv4 address: {IPv4}"); + } + + /// + /// Read IPv6 + /// + /// + public bool TryReadIPv6(out IPAddress address) + { + if (IPv6 == "*") + { + address = IPAddress.IPv6Any; + return true; + } + + if (IPv6 == "localhost") + { + address = IPAddress.IPv6Loopback; + return true; + } + + if (IPv6 == "disable") + { + address = IPAddress.None; + return true; + } + + if (IPAddress.TryParse(IPv6, out var ip)) + { + address = ip; + return true; + } + + throw new System.Exception($"Could not parse IPv6 address: {IPv6}"); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Configuration/WebSocketEndpointModel.cs b/Source/MQTTnet.Server/Configuration/WebSocketEndpointModel.cs new file mode 100644 index 00000000..50a1dae0 --- /dev/null +++ b/Source/MQTTnet.Server/Configuration/WebSocketEndpointModel.cs @@ -0,0 +1,18 @@ + +using System.Collections.Generic; + +namespace MQTTnet.Server.Configuration +{ + public class WebSocketEndPointModel + { + public bool Enabled { get; set; } = true; + + public string Path { get; set; } = "/mqtt"; + + public int ReceiveBufferSize { get; set; } = 4096; + + public int KeepAliveInterval { get; set; } = 120; + + public List AllowedOrigins { get; set; } + } +} diff --git a/Source/MQTTnet.Server/Controllers/ClientsController.cs b/Source/MQTTnet.Server/Controllers/ClientsController.cs new file mode 100644 index 00000000..afc75119 --- /dev/null +++ b/Source/MQTTnet.Server/Controllers/ClientsController.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using MQTTnet.Server.Mqtt; +using MQTTnet.Server.Status; + +namespace MQTTnet.Server.Controllers +{ + [ApiController] + public class ClientsController : ControllerBase + { + private readonly MqttServerService _mqttServerService; + + public ClientsController(MqttServerService mqttServerService) + { + _mqttServerService = mqttServerService ?? throw new ArgumentNullException(nameof(mqttServerService)); + } + + [Route("api/v1/clients")] + [HttpGet] + public async Task>> GetClients() + { + return new ObjectResult(await _mqttServerService.GetClientStatusAsync()); + } + + [Route("api/v1/clients/{clientId}")] + [HttpGet] + public async Task> GetClient(string clientId) + { + clientId = HttpUtility.UrlDecode(clientId); + + var client = (await _mqttServerService.GetClientStatusAsync()).FirstOrDefault(c => c.ClientId == clientId); + if (client == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + return new ObjectResult(client); + } + + [Route("api/v1/clients/{clientId}")] + [HttpDelete] + public async Task DeleteClient(string clientId) + { + clientId = HttpUtility.UrlDecode(clientId); + + var client = (await _mqttServerService.GetClientStatusAsync()).FirstOrDefault(c => c.ClientId == clientId); + if (client == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + await client.DisconnectAsync(); + return StatusCode((int)HttpStatusCode.NoContent); + } + } +} diff --git a/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs b/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs new file mode 100644 index 00000000..30110b38 --- /dev/null +++ b/Source/MQTTnet.Server/Controllers/RetainedApplicationMessagesController.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using MQTTnet.Server.Mqtt; + +namespace MQTTnet.Server.Controllers +{ + [ApiController] + public class RetainedApplicationMessagesController : ControllerBase + { + private readonly MqttServerService _mqttServerService; + + public RetainedApplicationMessagesController(MqttServerService mqttServerService) + { + _mqttServerService = mqttServerService ?? throw new ArgumentNullException(nameof(mqttServerService)); + } + + [Route("api/v1/retainedApplicationMessages")] + [HttpGet] + public async Task>> GetRetainedApplicationMessages() + { + return new ObjectResult(await _mqttServerService.GetRetainedApplicationMessagesAsync()); + } + + [Route("api/v1/retainedApplicationMessages/{topic}")] + [HttpGet] + public async Task> GetRetainedApplicationMessage(string topic) + { + topic = HttpUtility.UrlDecode(topic); + + var applicationMessage = (await _mqttServerService.GetRetainedApplicationMessagesAsync()).FirstOrDefault(c => c.Topic == topic); + if (applicationMessage == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + return new ObjectResult(applicationMessage); + } + + [Route("api/v1/retainedApplicationMessages")] + [HttpDelete] + public async Task DeleteRetainedApplicationMessages() + { + await _mqttServerService.ClearRetainedApplicationMessagesAsync(); + return StatusCode((int)HttpStatusCode.NoContent); + } + + [Route("api/v1/retainedApplicationMessages/{topic}")] + [HttpDelete] + public async Task DeleteRetainedApplicationMessage(string topic) + { + topic = HttpUtility.UrlDecode(topic); + + await _mqttServerService.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithRetainFlag().Build()); + return StatusCode((int)HttpStatusCode.NoContent); + } + } +} diff --git a/Source/MQTTnet.Server/Controllers/ScriptsController.cs b/Source/MQTTnet.Server/Controllers/ScriptsController.cs new file mode 100644 index 00000000..1b2a7032 --- /dev/null +++ b/Source/MQTTnet.Server/Controllers/ScriptsController.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MQTTnet.Server.Controllers +{ + public class ScriptsController + { + } +} diff --git a/Source/MQTTnet.Server/Controllers/SessionsController.cs b/Source/MQTTnet.Server/Controllers/SessionsController.cs new file mode 100644 index 00000000..415d4b9c --- /dev/null +++ b/Source/MQTTnet.Server/Controllers/SessionsController.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using MQTTnet.Server.Mqtt; +using MQTTnet.Server.Status; + +namespace MQTTnet.Server.Controllers +{ + [ApiController] + public class SessionsController : ControllerBase + { + private readonly MqttServerService _mqttServerService; + + public SessionsController(MqttServerService mqttServerService) + { + _mqttServerService = mqttServerService ?? throw new ArgumentNullException(nameof(mqttServerService)); + } + + [Route("api/v1/sessions")] + [HttpGet] + public async Task>> GetSessions() + { + return new ObjectResult(await _mqttServerService.GetSessionStatusAsync()); + } + + [Route("api/v1/sessions/{clientId}")] + [HttpGet] + public async Task> GetSession(string clientId) + { + clientId = HttpUtility.UrlDecode(clientId); + + var session = (await _mqttServerService.GetSessionStatusAsync()).FirstOrDefault(c => c.ClientId == clientId); + if (session == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + return new ObjectResult(session); + } + + [Route("api/v1/sessions/{clientId}")] + [HttpDelete] + public async Task DeleteSession(string clientId) + { + clientId = HttpUtility.UrlDecode(clientId); + + var session = (await _mqttServerService.GetSessionStatusAsync()).FirstOrDefault(c => c.ClientId == clientId); + if (session == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + await session.DeleteAsync(); + return StatusCode((int)HttpStatusCode.NoContent); + } + + [Route("api/v1/sessions/{clientId}/pendingApplicationMessages")] + [HttpDelete] + public async Task DeletePendingApplicationMessages(string clientId) + { + clientId = HttpUtility.UrlDecode(clientId); + + var session = (await _mqttServerService.GetSessionStatusAsync()).FirstOrDefault(c => c.ClientId == clientId); + if (session == null) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + await session.ClearPendingApplicationMessagesAsync(); + return StatusCode((int)HttpStatusCode.NoContent); + } + } +} diff --git a/Source/MQTTnet.Server/LICENSE b/Source/MQTTnet.Server/LICENSE new file mode 100644 index 00000000..7d84e2b1 --- /dev/null +++ b/Source/MQTTnet.Server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Christian Kratky + +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/Source/MQTTnet.Server/Logging/MqttNetChildLoggerWrapper.cs b/Source/MQTTnet.Server/Logging/MqttNetChildLoggerWrapper.cs new file mode 100644 index 00000000..7f351178 --- /dev/null +++ b/Source/MQTTnet.Server/Logging/MqttNetChildLoggerWrapper.cs @@ -0,0 +1,43 @@ +using System; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server.Logging +{ + public class MqttNetChildLoggerWrapper : IMqttNetChildLogger + { + private readonly MqttNetLoggerWrapper _logger; + private readonly string _source; + + public MqttNetChildLoggerWrapper(string source, MqttNetLoggerWrapper logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _source = source; + } + + public IMqttNetChildLogger CreateChildLogger(string source = null) + { + return _logger.CreateChildLogger(source); + } + + public void Verbose(string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Verbose, _source, message, parameters, null); + } + + public void Info(string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Info, _source, message, parameters, null); + } + + public void Warning(Exception exception, string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Warning, _source, message, parameters, exception); + } + + public void Error(Exception exception, string message, params object[] parameters) + { + _logger.Publish(MqttNetLogLevel.Error, _source, message, parameters, exception); + } + } +} diff --git a/Source/MQTTnet.Server/Logging/MqttNetLoggerWrapper.cs b/Source/MQTTnet.Server/Logging/MqttNetLoggerWrapper.cs new file mode 100644 index 00000000..3f521092 --- /dev/null +++ b/Source/MQTTnet.Server/Logging/MqttNetLoggerWrapper.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using Microsoft.Extensions.Logging; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Server.Logging +{ + public class MqttNetLoggerWrapper : IMqttNetLogger + { + private readonly ILogger _logger; + + public MqttNetLoggerWrapper(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public event EventHandler LogMessagePublished; + + public IMqttNetChildLogger CreateChildLogger(string source = null) + { + return new MqttNetChildLoggerWrapper(source, this); + } + + public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) + { + var convertedLogLevel = ConvertLogLevel(logLevel); + _logger.Log(convertedLogLevel, exception, message, parameters); + + var logMessagePublishedEvent = LogMessagePublished; + if (logMessagePublishedEvent != null) + { + var logMessage = new MqttNetLogMessage(null, DateTime.UtcNow, Thread.CurrentThread.ManagedThreadId, source, logLevel, message, exception); + logMessagePublishedEvent.Invoke(this, new MqttNetLogMessagePublishedEventArgs(logMessage)); + } + } + + private static LogLevel ConvertLogLevel(MqttNetLogLevel logLevel) + { + switch (logLevel) + { + case MqttNetLogLevel.Error: return LogLevel.Error; + case MqttNetLogLevel.Warning: return LogLevel.Warning; + case MqttNetLogLevel.Info: return LogLevel.Information; + case MqttNetLogLevel.Verbose: return LogLevel.Debug; + } + + return LogLevel.Debug; + } + } +} diff --git a/Source/MQTTnet.Server/MQTTnet.Server.csproj b/Source/MQTTnet.Server/MQTTnet.Server.csproj new file mode 100644 index 00000000..c322548c --- /dev/null +++ b/Source/MQTTnet.Server/MQTTnet.Server.csproj @@ -0,0 +1,74 @@ + + + + netcoreapp2.2 + InProcess + MQTTnet.Server + MQTTnet.Server + False + + MQTTnet + + + + false + false + latest + 3.3 + + + + + + + + + NU1605 + 1701;1702 + + + + + Always + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + diff --git a/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs b/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs new file mode 100644 index 00000000..06bf5467 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/CustomMqttFactory.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Server.Configuration; +using MQTTnet.Server.Logging; + +namespace MQTTnet.Server.Mqtt +{ + public class CustomMqttFactory + { + private readonly MqttFactory _mqttFactory; + + public CustomMqttFactory(SettingsModel settings, ILogger logger) + { + if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + // It is important to avoid injecting the logger wrapper to ensure that no + // unused log messages are generated by the MQTTnet library. Debug logging + // has a huge performance impact. + if (settings.EnableDebugLogging) + { + var mqttNetLogger = new MqttNetLoggerWrapper(logger); + _mqttFactory = new MqttFactory(mqttNetLogger); + + logger.LogWarning("Debug logging is enabled. Performance of MQTTnet Server is decreased!"); + } + else + { + _mqttFactory = new MqttFactory(); + } + + Logger = _mqttFactory.DefaultLogger; + + } + + public IMqttNetLogger Logger { get; } + + public IMqttServer CreateMqttServer(List adapters) + { + if (adapters == null) throw new ArgumentNullException(nameof(adapters)); + + return _mqttFactory.CreateMqttServer(adapters); + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttApplicationMessageInterceptor.cs b/Source/MQTTnet.Server/Mqtt/MqttApplicationMessageInterceptor.cs new file mode 100644 index 00000000..c5f3afd4 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttApplicationMessageInterceptor.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Protocol; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttApplicationMessageInterceptor : IMqttServerApplicationMessageInterceptor + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttApplicationMessageInterceptor(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context) + { + try + { + var pythonContext = new PythonDictionary + { + { "accept_publish", context.AcceptPublish }, + { "close_connection", context.CloseConnection }, + { "client_id", context.ClientId }, + { "topic", context.ApplicationMessage.Topic }, + { "qos", (int)context.ApplicationMessage.QualityOfServiceLevel }, + { "retain", context.ApplicationMessage.Retain } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_intercept_application_message", pythonContext); + + context.AcceptPublish = (bool)pythonContext.get("accept_publish", context.AcceptPublish); + context.CloseConnection = (bool)pythonContext.get("close_connection", context.CloseConnection); + context.ApplicationMessage.Topic = (string)pythonContext.get("topic", context.ApplicationMessage.Topic); + context.ApplicationMessage.QualityOfServiceLevel = (MqttQualityOfServiceLevel)(int)pythonContext.get("qos", (int)context.ApplicationMessage.QualityOfServiceLevel); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while intercepting application message."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttClientConnectedHandler.cs b/Source/MQTTnet.Server/Mqtt/MqttClientConnectedHandler.cs new file mode 100644 index 00000000..8d3879e7 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttClientConnectedHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttClientConnectedHandler : IMqttServerClientConnectedHandler + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttClientConnectedHandler(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task HandleClientConnectedAsync(MqttServerClientConnectedEventArgs eventArgs) + { + try + { + var pythonEventArgs = new PythonDictionary + { + { "client_id", eventArgs.ClientId } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_client_connected", pythonEventArgs); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while handling client connected event."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttClientDisconnectedHandler.cs b/Source/MQTTnet.Server/Mqtt/MqttClientDisconnectedHandler.cs new file mode 100644 index 00000000..f7b364e8 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttClientDisconnectedHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttClientDisconnectedHandler : IMqttServerClientDisconnectedHandler + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttClientDisconnectedHandler(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task HandleClientDisconnectedAsync(MqttServerClientDisconnectedEventArgs eventArgs) + { + try + { + var pythonEventArgs = new PythonDictionary + { + { "client_id", eventArgs.ClientId }, + { "type", PythonConvert.Pythonfy(eventArgs.DisconnectType) } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_client_disconnected", pythonEventArgs); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while handling client disconnected event."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttClientSubscribedTopicHandler.cs b/Source/MQTTnet.Server/Mqtt/MqttClientSubscribedTopicHandler.cs new file mode 100644 index 00000000..6d90b448 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttClientSubscribedTopicHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttClientSubscribedTopicHandler : IMqttServerClientSubscribedTopicHandler + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttClientSubscribedTopicHandler(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task HandleClientSubscribedTopicAsync(MqttServerClientSubscribedTopicEventArgs eventArgs) + { + try + { + var pythonEventArgs = new PythonDictionary + { + { "client_id", eventArgs.ClientId }, + { "topic", eventArgs.TopicFilter.Topic }, + { "qos", (int)eventArgs.TopicFilter.QualityOfServiceLevel } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_client_subscribed_topic", pythonEventArgs); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while handling client subscribed topic event."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttClientUnsubscribedTopicHandler.cs b/Source/MQTTnet.Server/Mqtt/MqttClientUnsubscribedTopicHandler.cs new file mode 100644 index 00000000..ac42f9ee --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttClientUnsubscribedTopicHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttClientUnsubscribedTopicHandler : IMqttServerClientUnsubscribedTopicHandler + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttClientUnsubscribedTopicHandler(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task HandleClientUnsubscribedTopicAsync(MqttServerClientUnsubscribedTopicEventArgs eventArgs) + { + try + { + var pythonEventArgs = new PythonDictionary + { + { "client_id", eventArgs.ClientId }, + { "topic", eventArgs.TopicFilter } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_client_unsubscribed_topic", pythonEventArgs); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while handling client unsubscribed topic event."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttConnectionValidator.cs b/Source/MQTTnet.Server/Mqtt/MqttConnectionValidator.cs new file mode 100644 index 00000000..83f66039 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttConnectionValidator.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Protocol; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttConnectionValidator : IMqttServerConnectionValidator + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttConnectionValidator(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task ValidateConnectionAsync(MqttConnectionValidatorContext context) + { + try + { + var pythonContext = new PythonDictionary + { + { "client_id", context.ClientId }, + { "endpoint", context.Endpoint }, + { "is_secure_connection", context.IsSecureConnection }, + { "username", context.Username }, + { "password", context.Password }, + { "result", PythonConvert.Pythonfy(context.ReturnCode) } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_validate_client_connection", pythonContext); + + context.ReturnCode = PythonConvert.ParseEnum((string)pythonContext["result"]); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while validating client connection."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttServerService.cs b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs new file mode 100644 index 00000000..42ec60b8 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MQTTnet.Adapter; +using MQTTnet.AspNetCore; +using MQTTnet.Client.Publishing; +using MQTTnet.Implementations; +using MQTTnet.Protocol; +using MQTTnet.Server.Configuration; +using MQTTnet.Server.Scripting; +using MQTTnet.Server.Status; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttServerService + { + private readonly ILogger _logger; + + private readonly SettingsModel _settings; + private readonly MqttApplicationMessageInterceptor _mqttApplicationMessageInterceptor; + private readonly MqttServerStorage _mqttServerStorage; + private readonly MqttClientConnectedHandler _mqttClientConnectedHandler; + private readonly MqttClientDisconnectedHandler _mqttClientDisconnectedHandler; + private readonly MqttClientSubscribedTopicHandler _mqttClientSubscribedTopicHandler; + private readonly MqttClientUnsubscribedTopicHandler _mqttClientUnsubscribedTopicHandler; + private readonly MqttConnectionValidator _mqttConnectionValidator; + private readonly IMqttServer _mqttServer; + private readonly MqttSubscriptionInterceptor _mqttSubscriptionInterceptor; + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly MqttWebSocketServerAdapter _webSocketServerAdapter; + + public MqttServerService( + SettingsModel settings, + CustomMqttFactory mqttFactory, + MqttClientConnectedHandler mqttClientConnectedHandler, + MqttClientDisconnectedHandler mqttClientDisconnectedHandler, + MqttClientSubscribedTopicHandler mqttClientSubscribedTopicHandler, + MqttClientUnsubscribedTopicHandler mqttClientUnsubscribedTopicHandler, + MqttConnectionValidator mqttConnectionValidator, + MqttSubscriptionInterceptor mqttSubscriptionInterceptor, + MqttApplicationMessageInterceptor mqttApplicationMessageInterceptor, + MqttServerStorage mqttServerStorage, + PythonScriptHostService pythonScriptHostService, + ILogger logger) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _mqttClientConnectedHandler = mqttClientConnectedHandler ?? throw new ArgumentNullException(nameof(mqttClientConnectedHandler)); + _mqttClientDisconnectedHandler = mqttClientDisconnectedHandler ?? throw new ArgumentNullException(nameof(mqttClientDisconnectedHandler)); + _mqttClientSubscribedTopicHandler = mqttClientSubscribedTopicHandler ?? throw new ArgumentNullException(nameof(mqttClientSubscribedTopicHandler)); + _mqttClientUnsubscribedTopicHandler = mqttClientUnsubscribedTopicHandler ?? throw new ArgumentNullException(nameof(mqttClientUnsubscribedTopicHandler)); + _mqttConnectionValidator = mqttConnectionValidator ?? throw new ArgumentNullException(nameof(mqttConnectionValidator)); + _mqttSubscriptionInterceptor = mqttSubscriptionInterceptor ?? throw new ArgumentNullException(nameof(mqttSubscriptionInterceptor)); + _mqttApplicationMessageInterceptor = mqttApplicationMessageInterceptor ?? throw new ArgumentNullException(nameof(mqttApplicationMessageInterceptor)); + _mqttServerStorage = mqttServerStorage ?? throw new ArgumentNullException(nameof(mqttServerStorage)); + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _webSocketServerAdapter = new MqttWebSocketServerAdapter(mqttFactory.Logger.CreateChildLogger()); + + var adapters = new List + { + new MqttTcpServerAdapter(mqttFactory.Logger.CreateChildLogger()), + _webSocketServerAdapter + }; + + _mqttServer = mqttFactory.CreateMqttServer(adapters); + } + + public void Configure() + { + _pythonScriptHostService.RegisterProxyObject("publish", new Action(Publish)); + + _mqttServerStorage.Configure(); + + _mqttServer.ClientConnectedHandler = _mqttClientConnectedHandler; + _mqttServer.ClientDisconnectedHandler = _mqttClientDisconnectedHandler; + _mqttServer.ClientSubscribedTopicHandler = _mqttClientSubscribedTopicHandler; + _mqttServer.ClientUnsubscribedTopicHandler = _mqttClientUnsubscribedTopicHandler; + + _mqttServer.StartAsync(CreateMqttServerOptions()).GetAwaiter().GetResult(); + + _logger.LogInformation("MQTT server started."); + } + + public Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext) + { + return _webSocketServerAdapter.RunWebSocketConnectionAsync(webSocket, httpContext); + } + + public Task> GetClientStatusAsync() + { + return _mqttServer.GetClientStatusAsync(); + } + + public Task> GetSessionStatusAsync() + { + return _mqttServer.GetSessionStatusAsync(); + } + + public Task ClearRetainedApplicationMessagesAsync() + { + return _mqttServer.ClearRetainedApplicationMessagesAsync(); + } + + public Task> GetRetainedApplicationMessagesAsync() + { + return _mqttServer.GetRetainedApplicationMessagesAsync(); + } + + public Task PublishAsync(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + return _mqttServer.PublishAsync(applicationMessage); + } + + private void Publish(PythonDictionary parameters) + { + try + { + var applicationMessageBuilder = new MqttApplicationMessageBuilder() + .WithTopic((string)parameters.get("topic", null)) + .WithRetainFlag((bool)parameters.get("retain", false)) + .WithQualityOfServiceLevel((MqttQualityOfServiceLevel)(int)parameters.get("qos", 0)); + + var payload = parameters.get("payload", null); + byte[] binaryPayload; + + if (payload == null) + { + binaryPayload = new byte[0]; + } + else if (payload is string stringPayload) + { + binaryPayload = Encoding.UTF8.GetBytes(stringPayload); + } + else if (payload is ByteArray byteArray) + { + binaryPayload = byteArray.ToArray(); + } + else if (payload is IEnumerable intArray) + { + binaryPayload = intArray.Select(Convert.ToByte).ToArray(); + } + else + { + throw new NotSupportedException("Payload type not supported."); + } + + applicationMessageBuilder = applicationMessageBuilder + .WithPayload(binaryPayload); + + var applicationMessage = applicationMessageBuilder.Build(); + + _mqttServer.PublishAsync(applicationMessage).GetAwaiter().GetResult(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while publishing application message from server."); + } + } + + private IMqttServerOptions CreateMqttServerOptions() + { + var options = new MqttServerOptionsBuilder() + .WithMaxPendingMessagesPerClient(_settings.MaxPendingMessagesPerClient) + .WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(_settings.CommunicationTimeout)) + .WithConnectionValidator(_mqttConnectionValidator) + .WithApplicationMessageInterceptor(_mqttApplicationMessageInterceptor) + .WithSubscriptionInterceptor(_mqttSubscriptionInterceptor) + .WithStorage(_mqttServerStorage); + + // Configure unencrypted connections + if (_settings.TcpEndPoint.Enabled) + { + options.WithDefaultEndpoint(); + + if (_settings.TcpEndPoint.TryReadIPv4(out var address4)) + { + options.WithDefaultEndpointBoundIPAddress(address4); + } + + if (_settings.TcpEndPoint.TryReadIPv6(out var address6)) + { + options.WithDefaultEndpointBoundIPV6Address(address6); + } + + if (_settings.TcpEndPoint.Port > 0) + { + options.WithDefaultEndpointPort(_settings.TcpEndPoint.Port); + } + } + else + { + options.WithoutDefaultEndpoint(); + } + + // Configure encrypted connections + if (_settings.EncryptedTcpEndPoint.Enabled) + { + options + .WithEncryptedEndpoint() + .WithEncryptionSslProtocol(SslProtocols.Tls12) + .WithEncryptionCertificate(_settings.EncryptedTcpEndPoint.ReadCertificate()); + + if (_settings.EncryptedTcpEndPoint.TryReadIPv4(out var address4)) + { + options.WithEncryptedEndpointBoundIPAddress(address4); + } + + if (_settings.EncryptedTcpEndPoint.TryReadIPv6(out var address6)) + { + options.WithEncryptedEndpointBoundIPV6Address(address6); + } + + if (_settings.EncryptedTcpEndPoint.Port > 0) + { + options.WithEncryptedEndpointPort(_settings.EncryptedTcpEndPoint.Port); + } + } + else + { + options.WithoutEncryptedEndpoint(); + } + + if (_settings.ConnectionBacklog > 0) + { + options.WithConnectionBacklog(_settings.ConnectionBacklog); + } + + if (_settings.EnablePersistentSessions) + { + options.WithPersistentSessions(); + } + + return options.Build(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs b/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs new file mode 100644 index 00000000..39b65d51 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttServerStorage.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Configuration; +using Newtonsoft.Json; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttServerStorage : IMqttServerStorage + { + private readonly List _messages = new List(); + + private readonly SettingsModel _settings; + private readonly ILogger _logger; + + private string _filename; + private bool _messagesHaveChanged; + + public MqttServerStorage(SettingsModel settings, ILogger logger) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Configure() + { + if (_settings.RetainedApplicationMessages?.Persist != true) + { + _logger.LogInformation("Persisting of retained application messages is disabled."); + return; + } + + _filename = ExpandFilename(); + + // The retained application messages are stored in a separate thread. + // This is mandatory because writing them to a slow storage (like RaspberryPi SD card) + // will slow down the whole message processing speed. + Task.Run(SaveRetainedMessagesInternalAsync, CancellationToken.None); + } + + public Task SaveRetainedMessagesAsync(IList messages) + { + lock (_messages) + { + _messages.Clear(); + _messages.AddRange(messages); + + _messagesHaveChanged = true; + } + + return Task.CompletedTask; + } + + private async Task SaveRetainedMessagesInternalAsync() + { + while (true) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_settings.RetainedApplicationMessages.WriteInterval)).ConfigureAwait(false); + + List messages; + lock (_messages) + { + if (!_messagesHaveChanged) + { + continue; + } + + messages = new List(_messages); + _messagesHaveChanged = false; + } + + var json = JsonConvert.SerializeObject(messages); + await File.WriteAllTextAsync(_filename, json, Encoding.UTF8).ConfigureAwait(false); + + _logger.LogInformation($"{messages.Count} retained MQTT messages written."); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while writing retained MQTT messages."); + } + } + } + + public async Task> LoadRetainedMessagesAsync() + { + if (_settings.RetainedApplicationMessages?.Persist != true) + { + return null; + } + + if (!File.Exists(_filename)) + { + return null; + } + + try + { + var json = await File.ReadAllTextAsync(_filename).ConfigureAwait(false); + var applicationMessages = JsonConvert.DeserializeObject>(json); + + _logger.LogInformation($"{applicationMessages.Count} retained MQTT messages loaded."); + + return applicationMessages; + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Error while loading persisted retained application messages."); + return null; + } + } + + private string ExpandFilename() + { + var filename = _settings.RetainedApplicationMessages.Filename; + + var uri = new Uri(filename, UriKind.RelativeOrAbsolute); + if (!uri.IsAbsoluteUri) + { + filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filename); + } + + return filename; + } + } +} diff --git a/Source/MQTTnet.Server/Mqtt/MqttSubscriptionInterceptor.cs b/Source/MQTTnet.Server/Mqtt/MqttSubscriptionInterceptor.cs new file mode 100644 index 00000000..2d37f747 --- /dev/null +++ b/Source/MQTTnet.Server/Mqtt/MqttSubscriptionInterceptor.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using IronPython.Runtime; +using Microsoft.Extensions.Logging; +using MQTTnet.Server.Scripting; + +namespace MQTTnet.Server.Mqtt +{ + public class MqttSubscriptionInterceptor : IMqttServerSubscriptionInterceptor + { + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public MqttSubscriptionInterceptor(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task InterceptSubscriptionAsync(MqttSubscriptionInterceptorContext context) + { + try + { + var pythonContext = new PythonDictionary + { + { "accept_subscription", context.AcceptSubscription }, + { "close_connection", context.CloseConnection }, + + { "client_id", context.ClientId }, + { "topic", context.TopicFilter.Topic }, + { "qos", (int)context.TopicFilter.QualityOfServiceLevel } + }; + + _pythonScriptHostService.InvokeOptionalFunction("on_intercept_subscription", pythonContext); + + context.AcceptSubscription = (bool)pythonContext["accept_subscription"]; + context.CloseConnection = (bool)pythonContext["close_connection"]; + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while intercepting subscription."); + } + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Server/Program.cs b/Source/MQTTnet.Server/Program.cs new file mode 100644 index 00000000..3e8a0a0a --- /dev/null +++ b/Source/MQTTnet.Server/Program.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace MQTTnet.Server +{ + public static class Program + { + public static int Main(string[] args) + { + try + { + PrintLogo(); + + WebHost.CreateDefaultBuilder(args).UseStartup().Build().Run(); + + return 0; + } + catch (Exception exception) + { + Console.WriteLine(exception); + return -1; + } + } + + private static void PrintLogo() + { + Console.ResetColor(); + Console.ForegroundColor = ConsoleColor.White; + Console.BackgroundColor = ConsoleColor.Red; + const string LogoText = +@"| | +| __ __ ____ _______ _______ _ _____ | +| | \/ |/ __ \__ __|__ __| | | / ____| | +| | \ / | | | | | | | |_ __ ___| |_ | (___ ___ _ ____ _____ _ __ | +| | |\/| | | | | | | | | '_ \ / _ \ __| \___ \ / _ \ '__\ \ / / _ \ '__| | +| | | | | |__| | | | | | | | | __/ |_ ____) | __/ | \ V / __/ | | +| |_| |_|\___\_\ |_| |_|_| |_|\___|\__| |_____/ \___|_| \_/ \___|_| | +| | +| |"; + + Console.WriteLine(LogoText); + Console.ResetColor(); + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(@" + -- The official MQTT server implementation of MQTTnet -- + Copyright (c) 2017-2019 The MQTTnet Team"); + + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(@" + https://github.com/chkr1011/MQTTnet"); + + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($@" + Version: {Assembly.GetExecutingAssembly().GetName().Version} + License: MIT (read LICENSE file) + Sponsoring: https://opencollective.com/mqttnet + Support: https://github.com/chkr1011/MQTTnet/issues + Docs: https://github.com/chkr1011/MQTTnet/wiki/MQTTnetServer +"); + + Console.BackgroundColor = ConsoleColor.White; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(" ! THIS IS AN ALPHA VERSION! IT IS NOT RECOMMENDED TO USE IT FOR ANY DIFFERENT PURPOSE THAN TESTING OR EVALUATING!"); + Console.WriteLine(); + Console.ResetColor(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/RetainedApplicationMessages.json b/Source/MQTTnet.Server/RetainedApplicationMessages.json new file mode 100644 index 00000000..04c16db3 --- /dev/null +++ b/Source/MQTTnet.Server/RetainedApplicationMessages.json @@ -0,0 +1 @@ +[{"Topic":"a","Payload":"YWFzYXNhc2E=","QualityOfServiceLevel":0,"Retain":true,"UserProperties":[],"ContentType":null,"ResponseTopic":null,"PayloadFormatIndicator":null,"MessageExpiryInterval":null,"TopicAlias":null,"CorrelationData":null,"SubscriptionIdentifier":null},{"Topic":"fgdfgd","Payload":"YWFzYXNhc2E=","QualityOfServiceLevel":0,"Retain":true,"UserProperties":[],"ContentType":null,"ResponseTopic":null,"PayloadFormatIndicator":null,"MessageExpiryInterval":null,"TopicAlias":null,"CorrelationData":null,"SubscriptionIdentifier":null}] \ No newline at end of file diff --git a/Source/MQTTnet.Server/Scripting/DataSharing/DataSharingService.cs b/Source/MQTTnet.Server/Scripting/DataSharing/DataSharingService.cs new file mode 100644 index 00000000..41b6854b --- /dev/null +++ b/Source/MQTTnet.Server/Scripting/DataSharing/DataSharingService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace MQTTnet.Server.Scripting.DataSharing +{ + public class DataSharingService + { + private readonly Dictionary _storage = new Dictionary(); + private readonly PythonScriptHostService _pythonScriptHostService; + private readonly ILogger _logger; + + public DataSharingService(PythonScriptHostService pythonScriptHostService, ILogger logger) + { + _pythonScriptHostService = pythonScriptHostService ?? throw new ArgumentNullException(nameof(pythonScriptHostService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Configure() + { + _pythonScriptHostService.RegisterProxyObject("write_shared_data", new Action(Write)); + _pythonScriptHostService.RegisterProxyObject("read_shared_data", new Func(Read)); + } + + public void Write(string key, object value) + { + lock (_storage) + { + _storage[key] = value; + _logger.LogInformation($"Shared data with key '{key}' updated."); + } + } + + public object Read(string key, object defaultValue) + { + lock (_storage) + { + if (!_storage.TryGetValue(key, out var value)) + { + return defaultValue; + } + + return value; + } + } + } +} diff --git a/Source/MQTTnet.Server/Scripting/PythonConvert.cs b/Source/MQTTnet.Server/Scripting/PythonConvert.cs new file mode 100644 index 00000000..532b691e --- /dev/null +++ b/Source/MQTTnet.Server/Scripting/PythonConvert.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Text; +using IronPython.Runtime; + +namespace MQTTnet.Server.Scripting +{ + public static class PythonConvert + { + public static object ToPython(object value) + { + if (value is PythonDictionary) + { + return value; + } + + if (value is string) + { + return value; + } + + if (value is int) + { + return value; + } + + if (value is float) + { + return value; + } + + if (value is bool) + { + return value; + } + + if (value is IDictionary dictionary) + { + var pythonDictionary = new PythonDictionary(); + foreach (DictionaryEntry dictionaryEntry in dictionary) + { + pythonDictionary.Add(dictionaryEntry.Key, ToPython(dictionaryEntry.Value)); + } + + return pythonDictionary; + } + + if (value is IEnumerable enumerable) + { + var pythonList = new List(); + foreach (var item in enumerable) + { + pythonList.Add(ToPython(item)); + } + + return pythonList; + } + + return value; + } + + public static string Pythonfy(Enum value) + { + return Pythonfy(value.ToString()); + } + + public static string Pythonfy(string value) + { + var result = new StringBuilder(); + foreach (var @char in value) + { + if (char.IsUpper(@char) && result.Length > 0) + { + result.Append('_'); + } + + result.Append(char.ToLowerInvariant(@char)); + } + + return result.ToString(); + } + + public static TEnum ParseEnum(string value) where TEnum : Enum + { + return (TEnum)Enum.Parse(typeof(TEnum), value.Replace("_", string.Empty), true); + } + } +} diff --git a/Source/MQTTnet.Server/Scripting/PythonIOStream.cs b/Source/MQTTnet.Server/Scripting/PythonIOStream.cs new file mode 100644 index 00000000..b346e896 --- /dev/null +++ b/Source/MQTTnet.Server/Scripting/PythonIOStream.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace MQTTnet.Server.Scripting +{ + public class PythonIOStream : Stream + { + private readonly ILogger _logger; + private readonly Encoding _encoder = Encoding.UTF8; + + public PythonIOStream(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + + if (count == 0) + { + return; + } + + var text = _encoder.GetString(buffer, offset, count); + if (text.Equals(Environment.NewLine)) + { + return; + } + + _logger.LogDebug(text); + } + + public override bool CanRead { get; } = false; + public override bool CanSeek { get; } = false; + public override bool CanWrite { get; } = true; + public override long Length { get; } = 0L; + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + } +} diff --git a/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs b/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs new file mode 100644 index 00000000..1acc58c4 --- /dev/null +++ b/Source/MQTTnet.Server/Scripting/PythonScriptHostService.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Scripting; +using Microsoft.Scripting.Hosting; + +namespace MQTTnet.Server.Scripting +{ + public class PythonScriptHostService + { + private readonly IDictionary _proxyObjects = new ExpandoObject(); + private readonly List _scriptInstances = new List(); + private readonly ILogger _logger; + private readonly ScriptEngine _scriptEngine; + + public PythonScriptHostService(PythonIOStream pythonIOStream, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scriptEngine = IronPython.Hosting.Python.CreateEngine(); + _scriptEngine.Runtime.IO.SetOutput(pythonIOStream, Encoding.UTF8); + } + + public void Configure() + { + AddSearchPaths(_scriptEngine); + + var scriptsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Scripts"); + foreach (var filename in Directory.GetFiles(scriptsDirectory, "*.py", SearchOption.AllDirectories).OrderBy(file => file)) + { + TryInitializeScript(filename); + } + } + + public void RegisterProxyObject(string name, object @object) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (@object == null) throw new ArgumentNullException(nameof(@object)); + + _proxyObjects.Add(name, @object); + } + + public void InvokeOptionalFunction(string name, object parameters) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + lock (_scriptInstances) + { + foreach (var pythonScriptInstance in _scriptInstances) + { + try + { + pythonScriptInstance.InvokeOptionalFunction(name, parameters); + } + catch (Exception exception) + { + _logger.LogError(exception, $"Error while invoking function '{name}' at script '{pythonScriptInstance.Name}'."); + } + } + } + } + + private void TryInitializeScript(string filename) + { + try + { + var scriptName = new FileInfo(filename).Name; + + _logger.LogTrace($"Initializing Python script '{scriptName}'..."); + var code = File.ReadAllText(filename); + + var scriptInstance = CreateScriptInstance(scriptName, code); + scriptInstance.InvokeOptionalFunction("initialize"); + + _scriptInstances.Add(scriptInstance); + + _logger.LogInformation($"Initialized script '{scriptName}'."); + } + catch (Exception exception) + { + _logger.LogError(exception, $"Error while initializing script '{new FileInfo(filename).Name}'."); + } + } + + private PythonScriptInstance CreateScriptInstance(string name, string code) + { + var scriptScope = _scriptEngine.CreateScope(); + + var source = scriptScope.Engine.CreateScriptSourceFromString(code, SourceCodeKind.File); + var compiledCode = source.Compile(); + + scriptScope.SetVariable("mqtt_net_server", _proxyObjects); + compiledCode.Execute(scriptScope); + + return new PythonScriptInstance(name, scriptScope); + } + + private void AddSearchPaths(ScriptEngine scriptEngine) + { + var paths = new List + { + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Lib"), + "/usr/lib/python2.7", + @"C:\Python27\Lib" + }; + + AddSearchPaths(scriptEngine, paths); + } + + private void AddSearchPaths(ScriptEngine scriptEngine, IEnumerable paths) + { + var searchPaths = scriptEngine.GetSearchPaths(); + + foreach (var path in paths) + { + if (Directory.Exists(path)) + { + searchPaths.Add(path); + _logger.LogInformation($"Added Python lib path: {path}"); + } + } + + scriptEngine.SetSearchPaths(searchPaths); + } + } +} diff --git a/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs b/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs new file mode 100644 index 00000000..91a46340 --- /dev/null +++ b/Source/MQTTnet.Server/Scripting/PythonScriptInstance.cs @@ -0,0 +1,57 @@ +using System; +using IronPython.Runtime; +using Microsoft.Scripting.Hosting; + +namespace MQTTnet.Server.Scripting +{ + public class PythonScriptInstance + { + private readonly ScriptScope _scriptScope; + + public PythonScriptInstance(string name, ScriptScope scriptScope) + { + _scriptScope = scriptScope; + + Name = name; + } + + public string Name { get; } + + public bool InvokeOptionalFunction(string name, params object[] parameters) + { + return InvokeOptionalFunction(name, parameters, out _); + } + + public bool InvokeOptionalFunction(string name, object[] parameters, out object result) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + lock (_scriptScope) + { + if (!_scriptScope.Engine.Operations.TryGetMember(_scriptScope, name, out var member)) + { + result = null; + return false; + } + + if (!(member is PythonFunction function)) + { + throw new Exception($"Member '{name}' is no Python function."); + } + + try + { + result = _scriptScope.Engine.Operations.Invoke(function, parameters); + return true; + } + catch (Exception exception) + { + var details = _scriptScope.Engine.GetService().FormatException(exception); + var message = $"Error while invoking function '{name}'. " + Environment.NewLine + details; + + throw new Exception(message, exception); + } + } + } + } +} diff --git a/Source/MQTTnet.Server/Scripts/00_sample.py b/Source/MQTTnet.Server/Scripts/00_sample.py new file mode 100644 index 00000000..845a2164 --- /dev/null +++ b/Source/MQTTnet.Server/Scripts/00_sample.py @@ -0,0 +1,107 @@ +import json + + +def initialize(): + """ + This function is invoked after the script file has been loaded. + It will be executed only one time. + """ + + print("Hello World from Sample script.") + + +def on_validate_client_connection(context): + """ + This function is invoked whenever a client wants to connect. It can be used to validate the connection. + """ + + print(context) + + mqtt_net_server.write_shared_data(context["client_id"], {"custom_value_1": 1, "custom_value_2": True}) + + return + + if context["client_id"] != "test_client": + context["result"] = "connection_refused_not_authorized" + return + + if context["username"] != "bud spencer": + context["result"] = "connection_refused_not_authorized" + return + + if context["password"] != "secret": + context["result"] = "connection_refused_not_authorized" + + print(context) + + +def on_intercept_subscription(context): + """ + This function is invoked whenever a client wants to subscribe to a topic. + """ + + print("Client '{client_id}' want's to subscribe to topic '{topic}'.".format(client_id=context["client_id"], topic=context["topic"])) + + +def on_intercept_application_message(context): + """ + This function is invoked for every processed application message. It also allows modifying + the message or cancel processing at all. + """ + + client_id = context["client_id"] + + if client_id != None: + shared_data = mqtt_net_server.read_shared_data(context["client_id"], {}) + print(shared_data) + + if context["topic"] == "topic_with_response": + + json_payload = { + "hello": "world", + "x": 1, + "y": True, + "z": None + } + + application_message = { + "retain": False, + "topic": "reply", + "payload": json.dumps(json_payload) + } + + mqtt_net_server.publish(application_message) + + print("Client '{client_id}' published topic '{topic}'.".format(client_id=context["client_id"], topic=context["topic"])) + + +def on_client_connected(event_args): + """ + This function is called whenever a client has passed the validation is connected. + """ + + print("Client '{client_id}' is now connected.".format(client_id=event_args["client_id"])) + + +def on_client_disconnected(event_args): + """ + This function is called whenever a client has disconnected. + """ + + print("Client '{client_id}' is now disconnected (type = {type}).".format(client_id=event_args["client_id"], type=event_args["type"])) + + +def on_client_subscribed_topic(event_args): + """ + This function is called whenever a client has subscribed to a topic (when allowed). + """ + + print("Client '{client_id}' has subscribed to '{topic}'.".format(client_id=event_args["client_id"], topic=event_args["topic"])) + + +def on_client_unsubscribed_topic(event_args): + """ + This function is called whenever a client has unsubscribed from a topic. + """ + + print("Client '{client_id}' has unsubscribed from '{topic}'.".format(client_id=event_args["client_id"], topic=event_args["topic"])) \ No newline at end of file diff --git a/Source/MQTTnet.Server/Scripts/readme.md b/Source/MQTTnet.Server/Scripts/readme.md new file mode 100644 index 00000000..fb4cc996 --- /dev/null +++ b/Source/MQTTnet.Server/Scripts/readme.md @@ -0,0 +1,19 @@ +This directory contains scripts which are loaded by the server and can be used to perform the following tasks. + +1. Validation of client connections via credentials, client IDs etc. +2. Manipulation of every processed message. +3. Validation of subscription attempts. +4. Publishing of custom application messages. + +All scripts are loaded and _MQTTnet Server_ will invoke functions according to predefined naming conventions. +If a function is implemented in multiple scripts the context will be moved throug all instances. This allows overriding of results or passing data to other (following) scripts. + +The Python starndard library ships with _MQTTnet Server_. But it is possible to add custom paths with Python libraries. + +``` +import sys +sys.path.append(PATH_TO_LIBRARY) +``` + +* All scripts must have the file extension _.py_. +* All scripts are sorted alphabetically (A to Z) before being loaded and parsed. \ No newline at end of file diff --git a/Source/MQTTnet.Server/Startup.cs b/Source/MQTTnet.Server/Startup.cs new file mode 100644 index 00000000..7135508e --- /dev/null +++ b/Source/MQTTnet.Server/Startup.cs @@ -0,0 +1,183 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Microsoft.Scripting.Utils; +using MQTTnet.Server.Configuration; +using MQTTnet.Server.Logging; +using MQTTnet.Server.Mqtt; +using MQTTnet.Server.Scripting; +using MQTTnet.Server.Scripting.DataSharing; +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace MQTTnet.Server +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + var builder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + public void Configure( + IApplicationBuilder application, + IHostingEnvironment environment, + MqttServerService mqttServerService, + PythonScriptHostService pythonScriptHostService, + DataSharingService dataSharingService, + SettingsModel settings) + { + if (environment.IsDevelopment()) + { + application.UseDeveloperExceptionPage(); + } + else + { + application.UseHsts(); + } + + application.UseStaticFiles(); + + application.UseHttpsRedirection(); + application.UseMvc(); + + ConfigureWebSocketEndpoint(application, mqttServerService, settings); + + dataSharingService.Configure(); + pythonScriptHostService.Configure(); + + mqttServerService.Configure(); + + application.UseSwagger(o => o.RouteTemplate = "/api/{documentName}/swagger.json"); + + application.UseSwaggerUI(o => + { + o.RoutePrefix = "api"; + o.DocumentTitle = "MQTTnet.Server API"; + o.SwaggerEndpoint("/api/v1/swagger.json", "MQTTnet.Server API v1"); + o.DisplayRequestDuration(); + o.DocExpansion(DocExpansion.List); + o.DefaultModelRendering(ModelRendering.Model); + }); + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) + .AddJsonOptions(options => + { + options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); + }); + + services.AddSingleton(ReadSettings()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSwaggerGen(c => + { + c.DescribeAllEnumsAsStrings(); + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "MQTTnet.Server API", + Version = "v1", + Description = "The public API for the MQTT broker MQTTnet.Server.", + License = new OpenApiLicense + { + Name = "MIT", + Url = new Uri("https://github.com/chkr1011/MQTTnet/blob/master/README.md") + }, + Contact = new OpenApiContact + { + Name = "MQTTnet.Server", + Email = string.Empty, + Url = new Uri("https://github.com/chkr1011/MQTTnet") + }, + }); + }); + } + + private SettingsModel ReadSettings() + { + var settings = new Configuration.SettingsModel(); + Configuration.Bind("MQTT", settings); + return settings; + } + + private static void ConfigureWebSocketEndpoint( + IApplicationBuilder application, + MqttServerService mqttServerService, + SettingsModel settings) + { + if (settings?.WebSocketEndPoint?.Enabled != true) + { + return; + } + + if (string.IsNullOrEmpty(settings.WebSocketEndPoint.Path)) + { + return; + } + + var webSocketOptions = new WebSocketOptions + { + KeepAliveInterval = TimeSpan.FromSeconds(settings.WebSocketEndPoint.KeepAliveInterval), + ReceiveBufferSize = settings.WebSocketEndPoint.ReceiveBufferSize + }; + + if (settings.WebSocketEndPoint.AllowedOrigins?.Any() == true) + { + webSocketOptions.AllowedOrigins.AddRange(settings.WebSocketEndPoint.AllowedOrigins); + } + + application.UseWebSockets(webSocketOptions); + + application.Use(async (context, next) => + { + if (context.Request.Path == settings.WebSocketEndPoint.Path) + { + if (context.WebSockets.IsWebSocketRequest) + { + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false)) + { + await mqttServerService.RunWebSocketConnectionAsync(webSocket, context).ConfigureAwait(false); + } + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next().ConfigureAwait(false); + } + }); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/appsettings.Development.json b/Source/MQTTnet.Server/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/Source/MQTTnet.Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Source/MQTTnet.Server/appsettings.json b/Source/MQTTnet.Server/appsettings.json new file mode 100644 index 00000000..2ee0eaf3 --- /dev/null +++ b/Source/MQTTnet.Server/appsettings.json @@ -0,0 +1,58 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://localhost:80" + }, + "Https": { + "Url": "http://localhost:443" + } + } + }, + "MQTT": { + /* + Wildcard Addresses: + * - All local IP addresses + localhost - Localhost only + disable - Skip address assignment + */ + "TcpEndPoint": { + "Enabled": true, + "IPv4": "*", + "IPv6": "*", + "Port": 1883 + }, + "EncryptedTcpEndPoint": { + "Enabled": false, + "IPv4": "*", + "IPv6": "*", + "Port": 8883, + "CertificatePath": "/absolute/path/to/pfx" + }, + "WebSocketEndPoint": { + "Enabled": true, + "Path": "/mqtt", + "KeepAliveInterval": 120, // In seconds. + "ReceiveBufferSize": 4096, + "AllowedOrigins": [] // List of strings with URLs. + }, + "CommunicationTimeout": 15, // In seconds. + "ConnectionBacklog": 0, /* Set 0 to disable */ + "EnablePersistentSessions": false, + "MaxPendingMessagesPerClient": 250, + "RetainedApplicationMessages": { + "Persist": true, + "Filename": "RetainedApplicationMessages.json", + "WriteInterval": 10 // In seconds. + }, + "EnableDebugLogging": true + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Source/MQTTnet.Server/package.json b/Source/MQTTnet.Server/package.json new file mode 100644 index 00000000..5b2909f1 --- /dev/null +++ b/Source/MQTTnet.Server/package.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "name": "mqtt.test", + "private": true, + "devDependencies": {}, + "dependencies": { + "@types/node": "^8.0.46", + "mqtt": "^2.15.1", + "systemjs": "^0.20.19", + "typescript": "^2.3.4" + } +} diff --git a/Source/MQTTnet.Server/run.bat b/Source/MQTTnet.Server/run.bat new file mode 100644 index 00000000..016447e6 --- /dev/null +++ b/Source/MQTTnet.Server/run.bat @@ -0,0 +1,2 @@ +@echo off +START "MQTTnet Server" dotnet .\MQTTnet.Server.dll \ No newline at end of file diff --git a/Source/MQTTnet.Server/run.sh b/Source/MQTTnet.Server/run.sh new file mode 100644 index 00000000..1f755ad2 --- /dev/null +++ b/Source/MQTTnet.Server/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "Starting MQTTnet Server.." +dotnet .\MQTTnet.Server.dll \ No newline at end of file diff --git a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs index 43e5c8c4..08bc8098 100644 --- a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs @@ -1,8 +1,8 @@ using System; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Formatter; using MQTTnet.Packets; -using MQTTnet.Serializer; namespace MQTTnet.Adapter { @@ -10,17 +10,23 @@ namespace MQTTnet.Adapter { string Endpoint { get; } - IMqttPacketSerializer PacketSerializer { get; } + bool IsSecureConnection { get; } - event EventHandler ReadingPacketStarted; + MqttPacketFormatterAdapter PacketFormatterAdapter { get; } - event EventHandler ReadingPacketCompleted; + long BytesSent { get; } + + long BytesReceived { get; } + + Action ReadingPacketStartedCallback { get; set; } + + Action ReadingPacketCompletedCallback { get; set; } Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken); Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken); - Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken); + Task SendPacketAsync(MqttBasePacket packet, TimeSpan timeout, CancellationToken cancellationToken); Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken); } diff --git a/Source/MQTTnet/Client/IMqttClientAdapterFactory.cs b/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs similarity index 76% rename from Source/MQTTnet/Client/IMqttClientAdapterFactory.cs rename to Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs index 197858b8..cb0988d8 100644 --- a/Source/MQTTnet/Client/IMqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs @@ -1,7 +1,7 @@ -using MQTTnet.Adapter; +using MQTTnet.Client.Options; using MQTTnet.Diagnostics; -namespace MQTTnet.Client +namespace MQTTnet.Adapter { public interface IMqttClientAdapterFactory { diff --git a/Source/MQTTnet/Adapter/IMqttServerAdapter.cs b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs index eff9eab7..52cda51d 100644 --- a/Source/MQTTnet/Adapter/IMqttServerAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs @@ -6,7 +6,7 @@ namespace MQTTnet.Adapter { public interface IMqttServerAdapter : IDisposable { - event EventHandler ClientAccepted; + Func ClientHandler { get; set; } Task StartAsync(IMqttServerOptions options); Task StopAsync(); diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 13e66065..a4d71b39 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -7,9 +7,9 @@ using System.Threading.Tasks; using MQTTnet.Channel; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; +using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; -using MQTTnet.Serializer; namespace MQTTnet.Adapter { @@ -22,41 +22,53 @@ namespace MQTTnet.Adapter private readonly IMqttNetChildLogger _logger; private readonly IMqttChannel _channel; + private readonly MqttPacketReader _packetReader; private readonly byte[] _fixedHeaderBuffer = new byte[2]; - private readonly byte[] _singleByteBuffer = new byte[1]; - private bool _isDisposed; - public MqttChannelAdapter(IMqttChannel channel, IMqttPacketSerializer serializer, IMqttNetChildLogger logger) + private long _bytesReceived; + private long _bytesSent; + + public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, IMqttNetChildLogger logger) { if (logger == null) throw new ArgumentNullException(nameof(logger)); _channel = channel ?? throw new ArgumentNullException(nameof(channel)); - PacketSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter)); + + _packetReader = new MqttPacketReader(_channel); _logger = logger.CreateChildLogger(nameof(MqttChannelAdapter)); } public string Endpoint => _channel.Endpoint; - public IMqttPacketSerializer PacketSerializer { get; } + public bool IsSecureConnection => _channel.IsSecureConnection; - public event EventHandler ReadingPacketStarted; - public event EventHandler ReadingPacketCompleted; + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + public long BytesSent => Interlocked.Read(ref _bytesSent); + public long BytesReceived => Interlocked.Read(ref _bytesReceived); + + public Action ReadingPacketStartedCallback { get; set; } + public Action ReadingPacketCompletedCallback { get; set; } + public async Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) { ThrowIfDisposed(); try { - _logger.Verbose("Connecting [Timeout={0}]", timeout); - - await Internal.TaskExtensions - .TimeoutAfterAsync(ct => _channel.ConnectAsync(ct), timeout, cancellationToken) - .ConfigureAwait(false); + if (timeout == TimeSpan.Zero) + { + await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await MqttTaskTimeout.WaitAsync(t => _channel.ConnectAsync(t), timeout, cancellationToken).ConfigureAwait(false); + } } catch (Exception exception) { @@ -75,11 +87,15 @@ namespace MQTTnet.Adapter try { - _logger.Verbose("Disconnecting [Timeout={0}]", timeout); - - await Internal.TaskExtensions - .TimeoutAfterAsync(ct => _channel.DisconnectAsync(), timeout, cancellationToken) - .ConfigureAwait(false); + if (timeout == TimeSpan.Zero) + { + await _channel.DisconnectAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await MqttTaskTimeout.WaitAsync( + t => _channel.DisconnectAsync(t), timeout, cancellationToken).ConfigureAwait(false); + } } catch (Exception exception) { @@ -92,16 +108,28 @@ namespace MQTTnet.Adapter } } - public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public async Task SendPacketAsync(MqttBasePacket packet, TimeSpan timeout, CancellationToken cancellationToken) { await _writerSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var packetData = PacketSerializer.Serialize(packet); - await _channel.WriteAsync(packetData.Array, packetData.Offset, packetData.Count, cancellationToken).ConfigureAwait(false); - PacketSerializer.FreeBuffer(); + var packetData = PacketFormatterAdapter.Encode(packet); - _logger.Verbose("TX >>> {0}", packet); + if (timeout == TimeSpan.Zero) + { + await _channel.WriteAsync(packetData.Array, packetData.Offset, packetData.Count, cancellationToken).ConfigureAwait(false); + } + else + { + await MqttTaskTimeout.WaitAsync( + t => _channel.WriteAsync(packetData.Array, packetData.Offset, packetData.Count, t), timeout, cancellationToken).ConfigureAwait(false); + } + + Interlocked.Add(ref _bytesReceived, packetData.Count); + + PacketFormatterAdapter.FreeBuffer(); + + _logger.Verbose("TX ({0} bytes) >>> {1}", packetData.Count, packet); } catch (Exception exception) { @@ -125,14 +153,13 @@ namespace MQTTnet.Adapter try { ReceivedMqttPacket receivedMqttPacket; - - if (timeout > TimeSpan.Zero) + if (timeout == TimeSpan.Zero) { - receivedMqttPacket = await Internal.TaskExtensions.TimeoutAfterAsync(ReceiveAsync, timeout, cancellationToken).ConfigureAwait(false); + receivedMqttPacket = await ReceiveAsync(cancellationToken).ConfigureAwait(false); } else { - receivedMqttPacket = await ReceiveAsync(cancellationToken).ConfigureAwait(false); + receivedMqttPacket = await MqttTaskTimeout.WaitAsync(ReceiveAsync, timeout, cancellationToken).ConfigureAwait(false); } if (receivedMqttPacket == null || cancellationToken.IsCancellationRequested) @@ -140,13 +167,20 @@ namespace MQTTnet.Adapter return null; } - var packet = PacketSerializer.Deserialize(receivedMqttPacket); + Interlocked.Add(ref _bytesSent, receivedMqttPacket.TotalLength); + + if (PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.Unknown) + { + PacketFormatterAdapter.DetectProtocolVersion(receivedMqttPacket); + } + + var packet = PacketFormatterAdapter.Decode(receivedMqttPacket); if (packet == null) { throw new MqttProtocolViolationException("Received malformed packet."); } - _logger.Verbose("RX <<< {0}", packet); + _logger.Verbose("RX ({0} bytes) <<< {1}", receivedMqttPacket.TotalLength, packet); return packet; } @@ -166,17 +200,34 @@ namespace MQTTnet.Adapter return null; } + public void ResetStatistics() + { + Interlocked.Exchange(ref _bytesReceived, 0L); + Interlocked.Exchange(ref _bytesSent, 0L); + } + private async Task ReceiveAsync(CancellationToken cancellationToken) { - var fixedHeader = await MqttPacketReader.ReadFixedHeaderAsync(_channel, _fixedHeaderBuffer, _singleByteBuffer, cancellationToken).ConfigureAwait(false); + var readFixedHeaderResult = await _packetReader.ReadFixedHeaderAsync(_fixedHeaderBuffer, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } try { - ReadingPacketStarted?.Invoke(this, EventArgs.Empty); + if (readFixedHeaderResult.ConnectionClosed) + { + return null; + } + ReadingPacketStartedCallback?.Invoke(); + + var fixedHeader = readFixedHeaderResult.FixedHeader; if (fixedHeader.RemainingLength == 0) { - return new ReceivedMqttPacket(fixedHeader.Flags, null); + return new ReceivedMqttPacket(fixedHeader.Flags, null, 2); } var body = new byte[fixedHeader.RemainingLength]; @@ -191,25 +242,27 @@ namespace MQTTnet.Adapter chunkSize = bytesLeft; } -#if WINDOWS_UWP var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false); -#else - // async/await is not used to avoid the overhead of context switches. We assume that the reamining data - // has been sent from the sender directly after the initial bytes. - var readBytes = _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); -#endif - cancellationToken.ThrowIfCancellationRequested(); - ExceptionHelper.ThrowIfGracefulSocketClose(readBytes); + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (readBytes == 0) + { + return null; + } bodyOffset += readBytes; } while (bodyOffset < body.Length); - return new ReceivedMqttPacket(fixedHeader.Flags, new MqttPacketBodyReader(body, 0, body.Length)); + var bodyReader = new MqttPacketBodyReader(body, 0, body.Length); + return new ReceivedMqttPacket(fixedHeader.Flags, bodyReader, fixedHeader.TotalLength); } finally { - ReadingPacketCompleted?.Invoke(this, EventArgs.Empty); + ReadingPacketCompletedCallback?.Invoke(); } } @@ -230,15 +283,19 @@ namespace MQTTnet.Adapter private static bool IsWrappedException(Exception exception) { - return exception is TaskCanceledException || - exception is OperationCanceledException || + return exception is OperationCanceledException || exception is MqttCommunicationTimedOutException || exception is MqttCommunicationException; } private static void WrapException(Exception exception) { - if (exception is IOException && exception.InnerException is SocketException socketException) + if (exception is IOException && exception.InnerException is SocketException innerException) + { + exception = innerException; + } + + if (exception is SocketException socketException) { if (socketException.SocketErrorCode == SocketError.ConnectionAborted || socketException.SocketErrorCode == SocketError.OperationAborted) diff --git a/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs b/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs index 68445cfd..44d50ec1 100644 --- a/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs +++ b/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs @@ -1,16 +1,16 @@ -using MQTTnet.Exceptions; -using MQTTnet.Protocol; +using MQTTnet.Client.Connecting; +using MQTTnet.Exceptions; namespace MQTTnet.Adapter { public class MqttConnectingFailedException : MqttCommunicationException { - public MqttConnectingFailedException(MqttConnectReturnCode returnCode) - : base($"Connecting with MQTT server failed ({returnCode}).") + public MqttConnectingFailedException(MqttClientConnectResultCode resultCode) + : base($"Connecting with MQTT server failed ({resultCode.ToString()}).") { - ReturnCode = returnCode; + ResultCode = resultCode; } - public MqttConnectReturnCode ReturnCode { get; } + public MqttClientConnectResultCode ResultCode { get; } } } diff --git a/Source/MQTTnet/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs b/Source/MQTTnet/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs deleted file mode 100644 index 42852493..00000000 --- a/Source/MQTTnet/Adapter/MqttServerAdapterClientAcceptedEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Adapter -{ - public class MqttServerAdapterClientAcceptedEventArgs : EventArgs - { - public MqttServerAdapterClientAcceptedEventArgs(IMqttChannelAdapter client) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public IMqttChannelAdapter Client { get; } - - public Task SessionTask { get; set; } - } -} diff --git a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs index 3a92d4de..355ad96f 100644 --- a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs +++ b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs @@ -1,17 +1,20 @@ -using MQTTnet.Serializer; +using MQTTnet.Formatter; namespace MQTTnet.Adapter { public class ReceivedMqttPacket { - public ReceivedMqttPacket(byte fixedHeader, MqttPacketBodyReader body) + public ReceivedMqttPacket(byte fixedHeader, IMqttPacketBodyReader body, int totalLength) { FixedHeader = fixedHeader; Body = body; + TotalLength = totalLength; } - public byte FixedHeader { get; } + public byte FixedHeader { get; set; } - public MqttPacketBodyReader Body { get; } + public IMqttPacketBodyReader Body { get; } + + public int TotalLength { get; } } } diff --git a/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs b/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs deleted file mode 100644 index 81dddbf1..00000000 --- a/Source/MQTTnet/ApplicationMessagePublisherExtensions.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MQTTnet.Protocol; - -namespace MQTTnet -{ - public static class ApplicationMessagePublisherExtensions - { - public static async Task PublishAsync(this IApplicationMessagePublisher publisher, IEnumerable applicationMessages) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await publisher.PublishAsync(applicationMessage).ConfigureAwait(false); - } - } - - public static async Task PublishAsync(this IApplicationMessagePublisher publisher, params MqttApplicationMessage[] applicationMessages) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await publisher.PublishAsync(applicationMessage).ConfigureAwait(false); - } - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel) - .WithRetainFlag(retain)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, Func builder) - { - var message = builder(new MqttApplicationMessageBuilder()).Build(); - return publisher.PublishAsync(message); - } - } -} diff --git a/Source/MQTTnet/Channel/IMqttChannel.cs b/Source/MQTTnet/Channel/IMqttChannel.cs index 6050bcb1..4848b46d 100644 --- a/Source/MQTTnet/Channel/IMqttChannel.cs +++ b/Source/MQTTnet/Channel/IMqttChannel.cs @@ -7,9 +7,10 @@ namespace MQTTnet.Channel public interface IMqttChannel : IDisposable { string Endpoint { get; } + bool IsSecureConnection { get; } Task ConnectAsync(CancellationToken cancellationToken); - Task DisconnectAsync(); + 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/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs b/Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs new file mode 100644 index 00000000..8b7be27e --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Client.Connecting +{ + public interface IMqttClientConnectedHandler + { + Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Client/Connecting/MqttClientAuthenticateResult.cs b/Source/MQTTnet/Client/Connecting/MqttClientAuthenticateResult.cs new file mode 100644 index 00000000..db6e5be5 --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientAuthenticateResult.cs @@ -0,0 +1,9 @@ +namespace MQTTnet.Client.Connecting +{ + public class MqttClientAuthenticateResult + { + public bool IsSessionPresent { get; set; } + + public MqttClientConnectResultCode ResultCode { get; set; } + } +} diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs new file mode 100644 index 00000000..a0dbae7b --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs @@ -0,0 +1,28 @@ +namespace MQTTnet.Client.Connecting +{ + 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/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs new file mode 100644 index 00000000..e7f97b9e --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace MQTTnet.Client.Connecting +{ + public class MqttClientConnectedEventArgs : EventArgs + { + public MqttClientConnectedEventArgs(MqttClientAuthenticateResult authenticateResult) + { + AuthenticateResult = authenticateResult ?? throw new ArgumentNullException(nameof(authenticateResult)); + } + + public MqttClientAuthenticateResult AuthenticateResult { get; } + } +} diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs new file mode 100644 index 00000000..96812afe --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Client.Connecting +{ + public class MqttClientConnectedHandlerDelegate : IMqttClientConnectedHandler + { + private readonly Func _handler; + + public MqttClientConnectedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = context => + { + handler(context); + return Task.FromResult(0); + }; + } + + public MqttClientConnectedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs b/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs new file mode 100644 index 00000000..14347bf0 --- /dev/null +++ b/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Client.Disconnecting +{ + public interface IMqttClientDisconnectedHandler + { + Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs new file mode 100644 index 00000000..995806e7 --- /dev/null +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs @@ -0,0 +1,9 @@ +namespace MQTTnet.Client.Disconnecting +{ + public class MqttClientDisconnectOptions + { + public MqttClientDisconnectReason ReasonCode { get; set; } = MqttClientDisconnectReason.NormalDisconnection; + + public string ReasonString { get; set; } + } +} diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs new file mode 100644 index 00000000..3e1bb661 --- /dev/null +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs @@ -0,0 +1,8 @@ +namespace MQTTnet.Client.Disconnecting +{ + public enum MqttClientDisconnectReason + { + NormalDisconnection = 0, + + } +} diff --git a/Source/MQTTnet/Client/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs similarity index 90% rename from Source/MQTTnet/Client/MqttClientDisconnectedEventArgs.cs rename to Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs index 2161cc26..7506042c 100644 --- a/Source/MQTTnet/Client/MqttClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Client +namespace MQTTnet.Client.Disconnecting { public class MqttClientDisconnectedEventArgs : EventArgs { diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs new file mode 100644 index 00000000..c02443ed --- /dev/null +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Client.Disconnecting +{ + public class MqttClientDisconnectedHandlerDelegate : IMqttClientDisconnectedHandler + { + private readonly Func _handler; + + public MqttClientDisconnectedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = context => + { + handler(context); + return Task.FromResult(0); + }; + } + + public MqttClientDisconnectedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs index b60a31a8..70259cf6 100644 --- a/Source/MQTTnet/Client/IMqttClient.cs +++ b/Source/MQTTnet/Client/IMqttClient.cs @@ -1,6 +1,11 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; namespace MQTTnet.Client { @@ -9,13 +14,14 @@ namespace MQTTnet.Client bool IsConnected { get; } IMqttClientOptions Options { get; } - event EventHandler Connected; - event EventHandler Disconnected; + IMqttClientConnectedHandler ConnectedHandler { get; set; } + + IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } + + Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken); + Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken); - Task ConnectAsync(IMqttClientOptions options); - Task DisconnectAsync(); - - Task> SubscribeAsync(IEnumerable topicFilters); - Task UnsubscribeAsync(IEnumerable topics); + Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken); + Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/IMqttClientFactory.cs b/Source/MQTTnet/Client/IMqttClientFactory.cs index cd687afc..95d567ea 100644 --- a/Source/MQTTnet/Client/IMqttClientFactory.cs +++ b/Source/MQTTnet/Client/IMqttClientFactory.cs @@ -1,4 +1,5 @@ -using MQTTnet.Diagnostics; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; namespace MQTTnet.Client { diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 0dc664da..10387bc7 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -1,13 +1,18 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; -using MQTTnet.Internal; +using MQTTnet.PacketDispatcher; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -16,16 +21,17 @@ namespace MQTTnet.Client public class MqttClient : IMqttClient { private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); + private readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); private readonly Stopwatch _sendTracker = new Stopwatch(); private readonly object _disconnectLock = new object(); - private readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); private readonly IMqttClientAdapterFactory _adapterFactory; private readonly IMqttNetChildLogger _logger; - private CancellationTokenSource _cancellationTokenSource; - internal Task _packetReceiverTask; - internal Task _keepAliveMessageSenderTask; + private CancellationTokenSource _backgroundCancellationTokenSource; + private Task _packetReceiverTask; + private Task _keepAlivePacketsSenderTask; + private IMqttChannelAdapter _adapter; private bool _cleanDisconnectInitiated; private long _disconnectGate; @@ -38,14 +44,17 @@ namespace MQTTnet.Client _logger = logger.CreateChildLogger(nameof(MqttClient)); } - public event EventHandler Connected; - public event EventHandler Disconnected; - public event EventHandler ApplicationMessageReceived; + public IMqttClientConnectedHandler ConnectedHandler { get; set; } + + public IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } + + public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } public bool IsConnected { get; private set; } + public IMqttClientOptions Options { get; private set; } - public async Task ConnectAsync(IMqttClientOptions options) + public async Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken) { if (options == null) throw new ArgumentNullException(nameof(options)); if (options.ChannelOptions == null) throw new ArgumentException("ChannelOptions are not set."); @@ -59,31 +68,39 @@ namespace MQTTnet.Client _packetIdentifierProvider.Reset(); _packetDispatcher.Reset(); - _cancellationTokenSource = new CancellationTokenSource(); - _disconnectGate = 0; - _adapter = _adapterFactory.CreateClientAdapter(options, _logger); + _backgroundCancellationTokenSource = new CancellationTokenSource(); + var backgroundCancellationToken = _backgroundCancellationTokenSource.Token; - _logger.Verbose($"Trying to connect with server ({Options.ChannelOptions})."); - await _adapter.ConnectAsync(Options.CommunicationTimeout, _cancellationTokenSource.Token).ConfigureAwait(false); + _disconnectGate = 0; + var adapter = _adapterFactory.CreateClientAdapter(options, _logger); + _adapter = adapter; + + _logger.Verbose($"Trying to connect with server '{options.ChannelOptions}' (Timeout={options.CommunicationTimeout})."); + await _adapter.ConnectAsync(options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); _logger.Verbose("Connection with server established."); - StartReceivingPackets(_cancellationTokenSource.Token); + _packetReceiverTask = Task.Run(() => TryReceivePacketsAsync(backgroundCancellationToken), backgroundCancellationToken); - var connectResponse = await AuthenticateAsync(options.WillMessage, _cancellationTokenSource.Token).ConfigureAwait(false); - _logger.Verbose("MQTT connection with server established."); + var authenticateResult = await AuthenticateAsync(adapter, options.WillMessage, cancellationToken).ConfigureAwait(false); _sendTracker.Restart(); if (Options.KeepAlivePeriod != TimeSpan.Zero) { - StartSendingKeepAliveMessages(_cancellationTokenSource.Token); + _keepAlivePacketsSenderTask = Task.Run(() => TrySendKeepAliveMessagesAsync(backgroundCancellationToken), backgroundCancellationToken); } IsConnected = true; - Connected?.Invoke(this, new MqttClientConnectedEventArgs(connectResponse.IsSessionPresent)); _logger.Info("Connected."); - return new MqttClientConnectResult(connectResponse.IsSessionPresent); + + var connectedHandler = ConnectedHandler; + if (connectedHandler != null) + { + await connectedHandler.HandleConnectedAsync(new MqttClientConnectedEventArgs(authenticateResult)).ConfigureAwait(false); + } + + return authenticateResult; } catch (Exception exception) { @@ -98,15 +115,16 @@ namespace MQTTnet.Client } } - public async Task DisconnectAsync() + public async Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken) { try { _cleanDisconnectInitiated = true; - if (IsConnected && !_cancellationTokenSource.IsCancellationRequested) + if (IsConnected) { - await SendAsync(new MqttDisconnectPacket(), _cancellationTokenSource.Token).ConfigureAwait(false); + var disconnectPacket = _adapter.PacketFormatterAdapter.DataConverter.CreateDisconnectPacket(options); + await SendAsync(disconnectPacket, cancellationToken).ConfigureAwait(false); } } finally @@ -118,64 +136,55 @@ namespace MQTTnet.Client } } - public async Task> SubscribeAsync(IEnumerable topicFilters) + public async Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken) { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + if (options == null) throw new ArgumentNullException(nameof(options)); ThrowIfNotConnected(); - var subscribePacket = new MqttSubscribePacket - { - PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(), - TopicFilters = topicFilters.ToList() - }; + var subscribePacket = _adapter.PacketFormatterAdapter.DataConverter.CreateSubscribePacket(options); + subscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); - var response = await SendAndReceiveAsync(subscribePacket, _cancellationTokenSource.Token).ConfigureAwait(false); - - if (response.SubscribeReturnCodes.Count != subscribePacket.TopicFilters.Count) - { - throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); - } - - return subscribePacket.TopicFilters.Select((t, i) => new MqttSubscribeResult(t, response.SubscribeReturnCodes[i])).ToList(); + var subAckPacket = await SendAndReceiveAsync(subscribePacket, cancellationToken).ConfigureAwait(false); + return _adapter.PacketFormatterAdapter.DataConverter.CreateClientSubscribeResult(subscribePacket, subAckPacket); } - public Task UnsubscribeAsync(IEnumerable topicFilters) + public async Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken) { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + if (options == null) throw new ArgumentNullException(nameof(options)); ThrowIfNotConnected(); - var unsubscribePacket = new MqttUnsubscribePacket - { - PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(), - TopicFilters = topicFilters.ToList() - }; + var unsubscribePacket = _adapter.PacketFormatterAdapter.DataConverter.CreateUnsubscribePacket(options); + unsubscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); - return SendAndReceiveAsync(unsubscribePacket, _cancellationTokenSource.Token); + var unsubAckPacket = await SendAndReceiveAsync(unsubscribePacket, cancellationToken).ConfigureAwait(false); + return _adapter.PacketFormatterAdapter.DataConverter.CreateClientUnsubscribeResult(unsubscribePacket, unsubAckPacket); } - public Task PublishAsync(MqttApplicationMessage applicationMessage) + public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + MqttTopicValidator.ThrowIfInvalid(applicationMessage.Topic); + ThrowIfNotConnected(); - var publishPacket = applicationMessage.ToPublishPacket(); + var publishPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePublishPacket(applicationMessage); switch (applicationMessage.QualityOfServiceLevel) { case MqttQualityOfServiceLevel.AtMostOnce: { - // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] - return SendAsync(publishPacket, _cancellationTokenSource.Token); + return PublishAtMostOnce(publishPacket, cancellationToken); } case MqttQualityOfServiceLevel.AtLeastOnce: { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); - return SendAndReceiveAsync(publishPacket, _cancellationTokenSource.Token); + return PublishAtLeastOnceAsync(publishPacket, cancellationToken); } case MqttQualityOfServiceLevel.ExactlyOnce: { - return PublishExactlyOnce(publishPacket, _cancellationTokenSource.Token); + return PublishExactlyOnceAsync(publishPacket, cancellationToken); } default: { @@ -186,33 +195,31 @@ namespace MQTTnet.Client public void Dispose() { - _cancellationTokenSource?.Cancel(false); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; + _backgroundCancellationTokenSource?.Cancel(false); + _backgroundCancellationTokenSource?.Dispose(); + _backgroundCancellationTokenSource = null; _adapter?.Dispose(); _adapter = null; } - private async Task AuthenticateAsync(MqttApplicationMessage willApplicationMessage, CancellationToken cancellationToken) + private async Task AuthenticateAsync(IMqttChannelAdapter channelAdapter, MqttApplicationMessage willApplicationMessage, CancellationToken cancellationToken) { - var connectPacket = new MqttConnectPacket - { - ClientId = Options.ClientId, - Username = Options.Credentials?.Username, - Password = Options.Credentials?.Password, - CleanSession = Options.CleanSession, - KeepAlivePeriod = (ushort)Options.KeepAlivePeriod.TotalSeconds, - WillMessage = willApplicationMessage - }; + var connectPacket = channelAdapter.PacketFormatterAdapter.DataConverter.CreateConnectPacket( + willApplicationMessage, + Options); - var response = await SendAndReceiveAsync(connectPacket, cancellationToken).ConfigureAwait(false); - if (response.ConnectReturnCode != MqttConnectReturnCode.ConnectionAccepted) + var connAckPacket = await SendAndReceiveAsync(connectPacket, cancellationToken).ConfigureAwait(false); + var result = channelAdapter.PacketFormatterAdapter.DataConverter.CreateClientConnectResult(connAckPacket); + + if (result.ResultCode != MqttClientConnectResultCode.Success) { - throw new MqttConnectingFailedException(response.ConnectReturnCode); + throw new MqttConnectingFailedException(result.ResultCode); } - return response; + _logger.Verbose("Authenticated MQTT connection with server established."); + + return result; } private void ThrowIfNotConnected() @@ -230,19 +237,20 @@ namespace MQTTnet.Client var clientWasConnected = IsConnected; InitiateDisconnect(); - + IsConnected = false; try { if (_adapter != null) { + _logger.Verbose("Disconnecting [Timeout={0}]", Options.CommunicationTimeout); await _adapter.DisconnectAsync(Options.CommunicationTimeout, CancellationToken.None).ConfigureAwait(false); } await WaitForTaskAsync(_packetReceiverTask, sender).ConfigureAwait(false); - await WaitForTaskAsync(_keepAliveMessageSenderTask, sender).ConfigureAwait(false); - + await WaitForTaskAsync(_keepAlivePacketsSenderTask, sender).ConfigureAwait(false); + _logger.Verbose("Disconnected from adapter."); } catch (Exception adapterException) @@ -255,7 +263,12 @@ namespace MQTTnet.Client _cleanDisconnectInitiated = false; _logger.Info("Disconnected."); - Disconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientWasConnected, exception)); + + var disconnectedHandler = DisconnectedHandler; + if (disconnectedHandler != null) + { + await disconnectedHandler.HandleDisconnectedAsync(new MqttClientDisconnectedEventArgs(clientWasConnected, exception)).ConfigureAwait(false); + } } } @@ -265,12 +278,12 @@ namespace MQTTnet.Client { try { - if (_cancellationTokenSource?.IsCancellationRequested == true) + if (_backgroundCancellationTokenSource?.IsCancellationRequested == true) { return; } - _cancellationTokenSource?.Cancel(false); + _backgroundCancellationTokenSource?.Cancel(false); } catch (Exception exception) { @@ -281,11 +294,14 @@ namespace MQTTnet.Client private Task SendAsync(MqttBasePacket packet, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromResult(0); + } _sendTracker.Restart(); - return _adapter.SendPacketAsync(packet, cancellationToken); + return _adapter.SendPacketAsync(packet, Options.CommunicationTimeout, cancellationToken); } private async Task SendAndReceiveAsync(MqttBasePacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttBasePacket @@ -300,26 +316,22 @@ namespace MQTTnet.Client identifier = packetWithIdentifier.PacketIdentifier.Value; } - var packetAwaiter = _packetDispatcher.AddPacketAwaiter(identifier); - try + using (var packetAwaiter = _packetDispatcher.AddPacketAwaiter(identifier)) { - await _adapter.SendPacketAsync(requestPacket, cancellationToken).ConfigureAwait(false); - var respone = await Internal.TaskExtensions.TimeoutAfterAsync(ct => packetAwaiter.Task, Options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); - - return (TResponsePacket)respone; - } - catch (MqttCommunicationTimedOutException) - { - _logger.Warning(null, "Timeout while waiting for packet of type '{0}'.", typeof(TResponsePacket).Namespace); - throw; - } - finally - { - _packetDispatcher.RemovePacketAwaiter(identifier); + try + { + await _adapter.SendPacketAsync(requestPacket, Options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); + return await packetAwaiter.WaitOneAsync(Options.CommunicationTimeout).ConfigureAwait(false); + } + catch (MqttCommunicationTimedOutException) + { + _logger.Warning(null, "Timeout while waiting for packet of type '{0}'.", typeof(TResponsePacket).Name); + throw; + } } } - private async Task SendKeepAliveMessagesAsync(CancellationToken cancellationToken) + private async Task TrySendKeepAliveMessagesAsync(CancellationToken cancellationToken) { try { @@ -364,7 +376,7 @@ namespace MQTTnet.Client if (!DisconnectIsPending()) { - await DisconnectInternalAsync(_keepAliveMessageSenderTask, exception).ConfigureAwait(false); + await DisconnectInternalAsync(_keepAlivePacketsSenderTask, exception).ConfigureAwait(false); } } finally @@ -373,7 +385,7 @@ namespace MQTTnet.Client } } - private async Task ReceivePacketsAsync(CancellationToken cancellationToken) + private async Task TryReceivePacketsAsync(CancellationToken cancellationToken) { try { @@ -381,13 +393,24 @@ namespace MQTTnet.Client while (!cancellationToken.IsCancellationRequested) { - var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken) - .ConfigureAwait(false); + var packet = await _adapter.ReceivePacketAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); - if (packet != null && !cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) { - await ProcessReceivedPacketAsync(packet, cancellationToken).ConfigureAwait(false); + return; } + + if (packet == null) + { + if (!DisconnectIsPending()) + { + await DisconnectInternalAsync(_packetReceiverTask, null).ConfigureAwait(false); + } + + return; + } + + await TryProcessReceivedPacketAsync(packet, cancellationToken).ConfigureAwait(false); } } catch (Exception exception) @@ -422,103 +445,99 @@ namespace MQTTnet.Client } } - private Task ProcessReceivedPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) - { - if (packet is MqttPublishPacket publishPacket) - { - return ProcessReceivedPublishPacketAsync(publishPacket, cancellationToken); - } - - if (packet is MqttPingReqPacket) - { - return SendAsync(new MqttPingRespPacket(), cancellationToken); - } - - if (packet is MqttDisconnectPacket) - { - return DisconnectAsync(); - } - - if (packet is MqttPubRelPacket pubRelPacket) - { - return ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken); - } - - _packetDispatcher.Dispatch(packet); - return Task.FromResult(0); - } - - private Task ProcessReceivedPublishPacketAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) - { - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) - { - FireApplicationMessageReceivedEvent(publishPacket); - return Task.FromResult(0); - } - - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) - { - FireApplicationMessageReceivedEvent(publishPacket); - return SendAsync(new MqttPubAckPacket { PacketIdentifier = publishPacket.PacketIdentifier }, cancellationToken); - } - - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) - { - // QoS 2 is implement as method "B" (4.3.3 QoS 2: Exactly once delivery) - FireApplicationMessageReceivedEvent(publishPacket); - return SendAsync(new MqttPubRecPacket { PacketIdentifier = publishPacket.PacketIdentifier }, cancellationToken); - } - - throw new MqttCommunicationException("Received a not supported QoS level."); - } - - private Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) - { - var response = new MqttPubCompPacket - { - PacketIdentifier = pubRelPacket.PacketIdentifier - }; - - return SendAsync(response, cancellationToken); - } - - private async Task PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) - { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); - - var pubRecPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); - var pubRelPacket = new MqttPubRelPacket - { - PacketIdentifier = pubRecPacket.PacketIdentifier - }; - - await SendAndReceiveAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); - } - - private void StartReceivingPackets(CancellationToken cancellationToken) - { - _packetReceiverTask = Task.Factory.StartNew( - () => ReceivePacketsAsync(cancellationToken), - cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default).Unwrap(); - } - - private void StartSendingKeepAliveMessages(CancellationToken cancellationToken) - { - _keepAliveMessageSenderTask = Task.Factory.StartNew( - () => SendKeepAliveMessagesAsync(cancellationToken), - cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default).Unwrap(); - } - - private void FireApplicationMessageReceivedEvent(MqttPublishPacket publishPacket) + private async Task TryProcessReceivedPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) { try { - var applicationMessage = publishPacket.ToApplicationMessage(); - ApplicationMessageReceived?.Invoke(this, new MqttApplicationMessageReceivedEventArgs(Options.ClientId, applicationMessage)); + if (packet is MqttPublishPacket publishPacket) + { + await TryProcessReceivedPublishPacketAsync(publishPacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttPubRelPacket pubRelPacket) + { + await SendAsync(new MqttPubCompPacket + { + PacketIdentifier = pubRelPacket.PacketIdentifier, + ReasonCode = MqttPubCompReasonCode.Success + }, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttPingReqPacket) + { + await SendAsync(new MqttPingRespPacket(), cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttDisconnectPacket) + { + await DisconnectAsync(null, cancellationToken).ConfigureAwait(false); + } + else + { + _packetDispatcher.Dispatch(packet); + } + } + catch (Exception exception) + { + if (_cleanDisconnectInitiated) + { + return; + } + + if (exception is OperationCanceledException) + { + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "MQTT communication exception while receiving packets."); + } + else + { + _logger.Error(exception, "Unhandled exception while receiving packets."); + } + + _packetDispatcher.Dispatch(exception); + + if (!DisconnectIsPending()) + { + await DisconnectInternalAsync(_packetReceiverTask, exception).ConfigureAwait(false); + } + } + } + + private async Task TryProcessReceivedPublishPacketAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + try + { + if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + await HandleReceivedApplicationMessageAsync(publishPacket).ConfigureAwait(false); + } + else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + if (await HandleReceivedApplicationMessageAsync(publishPacket).ConfigureAwait(false)) + { + await SendAsync(new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubAckReasonCode.Success + }, cancellationToken).ConfigureAwait(false); + } + } + else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + if (await HandleReceivedApplicationMessageAsync(publishPacket).ConfigureAwait(false)) + { + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubRecReasonCode.Success + }; + + await SendAsync(pubRecPacket, cancellationToken).ConfigureAwait(false); + } + } + else + { + throw new MqttProtocolViolationException("Received a not supported QoS level."); + } } catch (Exception exception) { @@ -526,6 +545,52 @@ namespace MQTTnet.Client } } + private async Task PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] + await SendAsync(publishPacket, cancellationToken).ConfigureAwait(false); + return _adapter.PacketFormatterAdapter.DataConverter.CreatePublishResult(null); + } + + private async Task PublishAtLeastOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + var response = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); + return _adapter.PacketFormatterAdapter.DataConverter.CreatePublishResult(response); + } + + private async Task PublishExactlyOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + + var pubRecPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); + + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubRelReasonCode.Success + }; + + var pubCompPacket = await SendAndReceiveAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); + + return _adapter.PacketFormatterAdapter.DataConverter.CreatePublishResult(pubRecPacket, pubCompPacket); + } + + private async Task HandleReceivedApplicationMessageAsync(MqttPublishPacket publishPacket) + { + var applicationMessage = _adapter.PacketFormatterAdapter.DataConverter.CreateApplicationMessage(publishPacket); + + var handler = ApplicationMessageReceivedHandler; + if (handler != null) + { + var eventArgs = new MqttApplicationMessageReceivedEventArgs(Options.ClientId, applicationMessage); + await handler.HandleApplicationMessageReceivedAsync(eventArgs).ConfigureAwait(false); + return !eventArgs.ProcessingFailed; + } + + return true; + } + private static async Task WaitForTaskAsync(Task task, Task sender) { if (task == sender || task == null) diff --git a/Source/MQTTnet/Client/MqttClientConnectResult.cs b/Source/MQTTnet/Client/MqttClientConnectResult.cs deleted file mode 100644 index 6044bf50..00000000 --- a/Source/MQTTnet/Client/MqttClientConnectResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MQTTnet.Client -{ - public class MqttClientConnectResult - { - public MqttClientConnectResult(bool isSessionPresent) - { - IsSessionPresent = isSessionPresent; - } - - public bool IsSessionPresent { get; } - } -} diff --git a/Source/MQTTnet/Client/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Client/MqttClientConnectedEventArgs.cs deleted file mode 100644 index c8c6ce56..00000000 --- a/Source/MQTTnet/Client/MqttClientConnectedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MQTTnet.Client -{ - public class MqttClientConnectedEventArgs : EventArgs - { - public MqttClientConnectedEventArgs(bool isSessionPresent) - { - IsSessionPresent = isSessionPresent; - } - - public bool IsSessionPresent { get; } - } -} diff --git a/Source/MQTTnet/Client/MqttClientExtensions.cs b/Source/MQTTnet/Client/MqttClientExtensions.cs index a7548dee..e5e3cd75 100644 --- a/Source/MQTTnet/Client/MqttClientExtensions.cs +++ b/Source/MQTTnet/Client/MqttClientExtensions.cs @@ -1,22 +1,147 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; using MQTTnet.Protocol; namespace MQTTnet.Client { public static class MqttClientExtensions { - public static Task> SubscribeAsync(this IMqttClient client, params TopicFilter[] topicFilters) + public static IMqttClient UseConnectedHandler(this IMqttClient client, Func handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + } + + return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); + } + + public static IMqttClient UseConnectedHandler(this IMqttClient client, Action handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + } + + return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); + } + + public static IMqttClient UseConnectedHandler(this IMqttClient client, IMqttClientConnectedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.ConnectedHandler = handler; + return client; + } + + public static IMqttClient UseDisconnectedHandler(this IMqttClient client, Func handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + } + + return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); + } + + public static IMqttClient UseDisconnectedHandler(this IMqttClient client, Action handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + } + + return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); + } + + public static IMqttClient UseDisconnectedHandler(this IMqttClient client, IMqttClientDisconnectedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.DisconnectedHandler = handler; + return client; + } + + public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, Func handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, Action handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (handler == null) + { + return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, IMqttApplicationMessageReceivedHandler handler) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + client.ApplicationMessageReceivedHandler = handler; + return client; + } + + public static Task ReconnectAsync(this IMqttClient client) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + if (client.Options == null) + { + throw new InvalidOperationException("_ReconnectAsync_ can be used only if _ConnectAsync_ was called before."); + } + + return client.ConnectAsync(client.Options); + } + + public static Task DisconnectAsync(this IMqttClient client) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + return client.DisconnectAsync(null); + } + + public static Task SubscribeAsync(this IMqttClient client, params TopicFilter[] topicFilters) { if (client == null) throw new ArgumentNullException(nameof(client)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - return client.SubscribeAsync(topicFilters.ToList()); + var options = new MqttClientSubscribeOptions(); + options.TopicFilters.AddRange(topicFilters); + + return client.SubscribeAsync(options); } - public static Task> SubscribeAsync(this IMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + public static Task SubscribeAsync(this IMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) { if (client == null) throw new ArgumentNullException(nameof(client)); if (topic == null) throw new ArgumentNullException(nameof(topic)); @@ -24,7 +149,7 @@ namespace MQTTnet.Client return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); } - public static Task> SubscribeAsync(this IMqttClient client, string topic) + public static Task SubscribeAsync(this IMqttClient client, string topic) { if (client == null) throw new ArgumentNullException(nameof(client)); if (topic == null) throw new ArgumentNullException(nameof(topic)); @@ -32,12 +157,123 @@ namespace MQTTnet.Client return client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); } - public static Task UnsubscribeAsync(this IMqttClient client, params string[] topicFilters) + public static Task UnsubscribeAsync(this IMqttClient client, params string[] topicFilters) { if (client == null) throw new ArgumentNullException(nameof(client)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - return client.UnsubscribeAsync(topicFilters.ToList()); + var options = new MqttClientUnsubscribeOptions(); + options.TopicFilters.AddRange(topicFilters); + + return client.UnsubscribeAsync(options); + } + + public static Task ConnectAsync(this IMqttClient client, IMqttClientOptions options) + { + return client.ConnectAsync(options, CancellationToken.None); + } + + public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectOptions options) + { + return client.DisconnectAsync(options, CancellationToken.None); + } + + public static Task SubscribeAsync(this IMqttClient client, MqttClientSubscribeOptions options) + { + return client.SubscribeAsync(options, CancellationToken.None); + } + + public static Task UnsubscribeAsync(this IMqttClient client, MqttClientUnsubscribeOptions options) + { + return client.UnsubscribeAsync(options, CancellationToken.None); + } + + public static Task PublishAsync(this IMqttClient client, MqttApplicationMessage applicationMessage) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + return client.PublishAsync(applicationMessage, CancellationToken.None); + } + + public static async Task PublishAsync(this IMqttClient client, IEnumerable applicationMessages) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await client.PublishAsync(applicationMessage).ConfigureAwait(false); + } + } + + public static Task PublishAsync(this IMqttClient client, string topic) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .Build()); + } + + public static Task PublishAsync(this IMqttClient client, string topic, IEnumerable payload) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .Build()); + } + + public static Task PublishAsync(this IMqttClient client, string topic, string payload) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .Build()); + } + + public static Task PublishAsync(this IMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .Build()); + } + + public static Task PublishAsync(this IMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .WithRetainFlag(retain) + .Build()); + } + + public static Task PublishAsync(this IMqttClient client, string topic, string payload, bool retain) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return client.PublishAsync(new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag(retain) + .Build()); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs b/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs index dc099183..bfee3861 100644 --- a/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs +++ b/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs @@ -13,7 +13,7 @@ } } - public ushort GetNewPacketIdentifier() + public ushort GetNextPacketIdentifier() { lock (_syncRoot) { diff --git a/Source/MQTTnet/Client/MqttSubscribeResult.cs b/Source/MQTTnet/Client/MqttSubscribeResult.cs deleted file mode 100644 index 2b7c46da..00000000 --- a/Source/MQTTnet/Client/MqttSubscribeResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MQTTnet.Protocol; - -namespace MQTTnet.Client -{ - public class MqttSubscribeResult - { - public MqttSubscribeResult(TopicFilter topicFilter, MqttSubscribeReturnCode returnCode) - { - TopicFilter = topicFilter; - ReturnCode = returnCode; - } - - public TopicFilter TopicFilter { get; } - - public MqttSubscribeReturnCode ReturnCode { get; } - } -} diff --git a/Source/MQTTnet/Client/IMqttClientChannelOptions.cs b/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs similarity index 75% rename from Source/MQTTnet/Client/IMqttClientChannelOptions.cs rename to Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs index 22249632..729d12f0 100644 --- a/Source/MQTTnet/Client/IMqttClientChannelOptions.cs +++ b/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public interface IMqttClientChannelOptions { diff --git a/Source/MQTTnet/Client/IMqttClientCredentials.cs b/Source/MQTTnet/Client/Options/IMqttClientCredentials.cs similarity index 77% rename from Source/MQTTnet/Client/IMqttClientCredentials.cs rename to Source/MQTTnet/Client/Options/IMqttClientCredentials.cs index d214d64f..ec188673 100644 --- a/Source/MQTTnet/Client/IMqttClientCredentials.cs +++ b/Source/MQTTnet/Client/Options/IMqttClientCredentials.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public interface IMqttClientCredentials { diff --git a/Source/MQTTnet/Client/IMqttClientOptions.cs b/Source/MQTTnet/Client/Options/IMqttClientOptions.cs similarity index 52% rename from Source/MQTTnet/Client/IMqttClientOptions.cs rename to Source/MQTTnet/Client/Options/IMqttClientOptions.cs index 176f5e43..3150baa3 100644 --- a/Source/MQTTnet/Client/IMqttClientOptions.cs +++ b/Source/MQTTnet/Client/Options/IMqttClientOptions.cs @@ -1,7 +1,7 @@ using System; -using MQTTnet.Serializer; +using MQTTnet.Formatter; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public interface IMqttClientOptions { @@ -16,5 +16,15 @@ namespace MQTTnet.Client TimeSpan? KeepAliveSendInterval { get; } MqttApplicationMessage WillMessage { get; } + uint? WillDelayInterval { get; } + + string AuthenticationMethod { get; } + byte[] AuthenticationData { get; } + uint? MaximumPacketSize { get; } + ushort? ReceiveMaximum { get; } + bool? RequestProblemInformation { get; } + bool? RequestResponseInformation { get; } + uint? SessionExpiryInterval { get; } + ushort? TopicAliasMaximum { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttClientCredentials.cs b/Source/MQTTnet/Client/Options/MqttClientCredentials.cs similarity index 82% rename from Source/MQTTnet/Client/MqttClientCredentials.cs rename to Source/MQTTnet/Client/Options/MqttClientCredentials.cs index feaa1e6c..8836e364 100644 --- a/Source/MQTTnet/Client/MqttClientCredentials.cs +++ b/Source/MQTTnet/Client/Options/MqttClientCredentials.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientCredentials : IMqttClientCredentials { diff --git a/Source/MQTTnet/Client/MqttClientOptions.cs b/Source/MQTTnet/Client/Options/MqttClientOptions.cs similarity index 52% rename from Source/MQTTnet/Client/MqttClientOptions.cs rename to Source/MQTTnet/Client/Options/MqttClientOptions.cs index 7d3ad5d8..be9dc7b0 100644 --- a/Source/MQTTnet/Client/MqttClientOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptions.cs @@ -1,20 +1,31 @@ using System; -using MQTTnet.Serializer; +using MQTTnet.Formatter; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientOptions : IMqttClientOptions { public string ClientId { get; set; } = Guid.NewGuid().ToString("N"); public bool CleanSession { get; set; } = true; - public IMqttClientCredentials Credentials { get; set; } = new MqttClientCredentials(); + public IMqttClientCredentials Credentials { get; set; } public MqttProtocolVersion ProtocolVersion { get; set; } = MqttProtocolVersion.V311; - public IMqttClientChannelOptions ChannelOptions { get; set; } + public IMqttClientChannelOptions ChannelOptions { get; set; } public TimeSpan CommunicationTimeout { get; set; } = TimeSpan.FromSeconds(10); public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(15); public TimeSpan? KeepAliveSendInterval { get; set; } public MqttApplicationMessage WillMessage { get; set; } + public uint? WillDelayInterval { get; set; } + + public string AuthenticationMethod { get; set; } + public byte[] AuthenticationData { get; set; } + + public uint? MaximumPacketSize { get; set; } + public ushort? ReceiveMaximum { get; set; } + public bool? RequestProblemInformation { get; set; } + public bool? RequestResponseInformation { get; set; } + public uint? SessionExpiryInterval { get; set; } + public ushort? TopicAliasMaximum { get; set; } } } diff --git a/Source/MQTTnet/Client/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs similarity index 70% rename from Source/MQTTnet/Client/MqttClientOptionsBuilder.cs rename to Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index 85e2b4d8..20e008a3 100644 --- a/Source/MQTTnet/Client/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -1,8 +1,8 @@ using System; using System.Linq; -using MQTTnet.Serializer; +using MQTTnet.Formatter; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientOptionsBuilder { @@ -15,6 +15,11 @@ namespace MQTTnet.Client public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) { + if (value == MqttProtocolVersion.Unknown) + { + throw new ArgumentException("Protocol version is invalid."); + } + _options.ProtocolVersion = value; return this; } @@ -31,6 +36,11 @@ namespace MQTTnet.Client return this; } + public MqttClientOptionsBuilder WithNoKeepAlive() + { + return WithKeepAlivePeriod(TimeSpan.Zero); + } + public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) { _options.KeepAlivePeriod = value; @@ -55,6 +65,55 @@ namespace MQTTnet.Client return this; } + public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) + { + _options.AuthenticationMethod = method; + _options.AuthenticationData = data; + return this; + } + + public MqttClientOptionsBuilder WithWillDelayInterval(uint? willDelayInterval) + { + _options.WillDelayInterval = willDelayInterval; + return this; + } + + public MqttClientOptionsBuilder WithTopicAliasMaximum(ushort? topicAliasMaximum) + { + _options.TopicAliasMaximum = topicAliasMaximum; + return this; + } + + public MqttClientOptionsBuilder WithMaximumPacketSize(uint? maximumPacketSize) + { + _options.MaximumPacketSize = maximumPacketSize; + 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 WithCredentials(string username, string password = null) { _options.Credentials = new MqttClientCredentials @@ -92,11 +151,13 @@ namespace MQTTnet.Client return this; } - public MqttClientOptionsBuilder WithWebSocketServer(string uri) + public MqttClientOptionsBuilder WithWebSocketServer(string uri, MqttClientOptionsBuilderWebSocketParameters parameters = null) { _webSocketOptions = new MqttClientWebSocketOptions { - Uri = uri + Uri = uri, + RequestHeaders = parameters?.RequestHeaders, + CookieContainer = parameters?.CookieContainer }; return this; @@ -113,25 +174,6 @@ namespace MQTTnet.Client return WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true }); } - [Obsolete("Use method _WithTls_ which accepts the _MqttClientOptionsBuilderTlsParameters_.")] - public MqttClientOptionsBuilder WithTls( - bool allowUntrustedCertificates = false, - bool ignoreCertificateChainErrors = false, - bool ignoreCertificateRevocationErrors = false, - params byte[][] certificates) - { - _tlsParameters = new MqttClientOptionsBuilderTlsParameters - { - UseTls = true, - AllowUntrustedCertificates = allowUntrustedCertificates, - IgnoreCertificateChainErrors = ignoreCertificateChainErrors, - IgnoreCertificateRevocationErrors = ignoreCertificateRevocationErrors, - Certificates = certificates?.ToList() - }; - - return this; - } - public IMqttClientOptions Build() { if (_tcpOptions == null && _webSocketOptions == null) diff --git a/Source/MQTTnet/Client/MqttClientOptionsBuilderTlsParameters.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs similarity index 96% rename from Source/MQTTnet/Client/MqttClientOptionsBuilderTlsParameters.cs rename to Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs index 5764c797..ea36baa9 100644 --- a/Source/MQTTnet/Client/MqttClientOptionsBuilderTlsParameters.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs @@ -4,7 +4,7 @@ using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientOptionsBuilderTlsParameters { diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs new file mode 100644 index 00000000..f516e837 --- /dev/null +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Net; + +namespace MQTTnet.Client.Options +{ + public class MqttClientOptionsBuilderWebSocketParameters + { + public IDictionary RequestHeaders { get; set; } + + public CookieContainer CookieContainer { get; set; } + } +} diff --git a/Source/MQTTnet/Client/MqttClientTcpOptions.cs b/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs similarity index 54% rename from Source/MQTTnet/Client/MqttClientTcpOptions.cs rename to Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs index 4569be33..c7b0e809 100644 --- a/Source/MQTTnet/Client/MqttClientTcpOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs @@ -1,4 +1,6 @@ -namespace MQTTnet.Client +using System.Net.Sockets; + +namespace MQTTnet.Client.Options { public class MqttClientTcpOptions : IMqttClientChannelOptions { @@ -6,7 +8,13 @@ public int? Port { get; set; } - public int BufferSize { get; set; } = 4096; + public int BufferSize { get; set; } = 65536; + + public bool DualMode { get; set; } = true; + + public bool NoDelay { get; set; } = true; + + public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified; public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); diff --git a/Source/MQTTnet/Client/MqttClientTcpOptionsExtensions.cs b/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs similarity index 92% rename from Source/MQTTnet/Client/MqttClientTcpOptionsExtensions.cs rename to Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs index 460069a7..1b49a58d 100644 --- a/Source/MQTTnet/Client/MqttClientTcpOptionsExtensions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public static class MqttClientTcpOptionsExtensions { diff --git a/Source/MQTTnet/Client/MqttClientTlsOptions.cs b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs similarity index 95% rename from Source/MQTTnet/Client/MqttClientTlsOptions.cs rename to Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs index 73d65cbc..db4077da 100644 --- a/Source/MQTTnet/Client/MqttClientTlsOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Net.Security; -using System.Security.Cryptography.X509Certificates; using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientTlsOptions { diff --git a/Source/MQTTnet/Client/MqttClientWebSocketOptions.cs b/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs similarity index 95% rename from Source/MQTTnet/Client/MqttClientWebSocketOptions.cs rename to Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs index fe8f1d27..d62f1f17 100644 --- a/Source/MQTTnet/Client/MqttClientWebSocketOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Net; -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientWebSocketOptions : IMqttClientChannelOptions { diff --git a/Source/MQTTnet/Client/MqttClientWebSocketProxyOptions.cs b/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs similarity index 90% rename from Source/MQTTnet/Client/MqttClientWebSocketProxyOptions.cs rename to Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs index 13b13667..7ff40f84 100644 --- a/Source/MQTTnet/Client/MqttClientWebSocketProxyOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs @@ -1,4 +1,4 @@ -namespace MQTTnet.Client +namespace MQTTnet.Client.Options { public class MqttClientWebSocketProxyOptions { diff --git a/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs new file mode 100644 index 00000000..7b485c6b --- /dev/null +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs @@ -0,0 +1,15 @@ +namespace MQTTnet.Client.Publishing +{ + 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/MQTTnet/Client/Publishing/MqttClientPublishResult.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs new file mode 100644 index 00000000..15035ec0 --- /dev/null +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs @@ -0,0 +1,17 @@ + +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client.Publishing +{ + public class MqttClientPublishResult + { + public ushort? PacketIdentifier { get; set; } + + public MqttClientPublishReasonCode ReasonCode { get; set; } = MqttClientPublishReasonCode.Success; + + public string ReasonString { get; set; } + + public List UserProperties { get; set; } + } +} diff --git a/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs b/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs new file mode 100644 index 00000000..3ea73175 --- /dev/null +++ b/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Client.Receiving +{ + public interface IMqttApplicationMessageReceivedHandler + { + Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs new file mode 100644 index 00000000..1d73aaa1 --- /dev/null +++ b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Client.Receiving +{ + public class MqttApplicationMessageReceivedHandlerDelegate : IMqttApplicationMessageReceivedHandler + { + private readonly Func _handler; + + public MqttApplicationMessageReceivedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = context => + { + handler(context); + return Task.FromResult(0); + }; + } + + public MqttApplicationMessageReceivedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs context) + { + return _handler(context); + } + } +} diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs new file mode 100644 index 00000000..8d2adbf8 --- /dev/null +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client.Subscribing +{ + public class MqttClientSubscribeOptions + { + public List TopicFilters { get; set; } = new List(); + + public List UserProperties { get; set; } = new List(); + } +} diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs new file mode 100644 index 00000000..66d68d4e --- /dev/null +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MQTTnet.Client.Subscribing +{ + public class MqttClientSubscribeResult + { + public List Items { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs new file mode 100644 index 00000000..119ec4fa --- /dev/null +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs @@ -0,0 +1,18 @@ +namespace MQTTnet.Client.Subscribing +{ + public enum MqttClientSubscribeResultCode + { + GrantedQoS0 = 0, + GrantedQoS1 = 1, + GrantedQoS2 = 2, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + SharedSubscriptionsNotSupported = 158, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs new file mode 100644 index 00000000..286123f4 --- /dev/null +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Client.Subscribing +{ + public class MqttClientSubscribeResultItem + { + public MqttClientSubscribeResultItem(TopicFilter topicFilter, MqttClientSubscribeResultCode resultCode) + { + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + ResultCode = resultCode; + } + + public TopicFilter TopicFilter { get; } + + public MqttClientSubscribeResultCode ResultCode { get; } + } +} diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs new file mode 100644 index 00000000..73c21ef7 --- /dev/null +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client.Unsubscribing +{ + public class MqttClientUnsubscribeOptions + { + public List TopicFilters { get; set; } = new List(); + + public List UserProperties { get; set; } = new List(); + } +} diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs new file mode 100644 index 00000000..f3e4c4c9 --- /dev/null +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MQTTnet.Client.Unsubscribing +{ + public class MqttClientUnsubscribeResult + { + public List Items { get; } =new List(); + } +} diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs new file mode 100644 index 00000000..269abc10 --- /dev/null +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs @@ -0,0 +1,13 @@ +namespace MQTTnet.Client.Unsubscribing +{ + public enum MqttClientUnsubscribeResultCode + { + Success = 0, + NoSubscriptionExisted = 17, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145 + } +} diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs new file mode 100644 index 00000000..755ee43c --- /dev/null +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Client.Unsubscribing +{ + public class MqttClientUnsubscribeResultItem + { + public MqttClientUnsubscribeResultItem(string topicFilter, MqttClientUnsubscribeResultCode reasonCode) + { + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + ReasonCode = reasonCode; + } + + public string TopicFilter { get; } + + public MqttClientUnsubscribeResultCode ReasonCode { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs deleted file mode 100644 index 21a66b90..00000000 --- a/Source/MQTTnet/Exceptions/MqttCommunicationClosedGracefullyException.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Exceptions -{ - public class MqttCommunicationClosedGracefullyException : MqttCommunicationException - { - } -} diff --git a/Source/MQTTnet/Formatter/IMqttDataConverter.cs b/Source/MQTTnet/Formatter/IMqttDataConverter.cs new file mode 100644 index 00000000..c4540e73 --- /dev/null +++ b/Source/MQTTnet/Formatter/IMqttDataConverter.cs @@ -0,0 +1,37 @@ +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public interface IMqttDataConverter + { + MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage); + + MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket); + + MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket); + + MqttClientAuthenticateResult CreateClientConnectResult(MqttConnAckPacket connAckPacket); + + MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options); + + MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket); + + MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket); + + MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options); + + MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options); + + MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options); + + MqttClientPublishResult CreatePublishResult(MqttPubAckPacket pubAckPacket); + + MqttClientPublishResult CreatePublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket); + } +} diff --git a/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs b/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs new file mode 100644 index 00000000..632c4931 --- /dev/null +++ b/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs @@ -0,0 +1,31 @@ +using System; + +namespace MQTTnet.Formatter +{ + public interface IMqttPacketBodyReader + { + ulong Length { get; } + + ulong Offset { get; } + + bool EndOfStream { get; } + + byte ReadByte(); + + byte[] ReadRemainingData(); + + ushort ReadTwoByteInteger(); + + string ReadStringWithLengthPrefix(); + + byte[] ReadWithLengthPrefix(); + + uint ReadFourByteInteger(); + + uint ReadVariableLengthInteger(); + + bool ReadBoolean(); + + void Seek(ulong position); + } +} diff --git a/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs new file mode 100644 index 00000000..6dd417c5 --- /dev/null +++ b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs @@ -0,0 +1,17 @@ +using System; +using MQTTnet.Adapter; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public interface IMqttPacketFormatter + { + IMqttDataConverter DataConverter { get; } + + ArraySegment Encode(MqttBasePacket mqttPacket); + + MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket); + + void FreeBuffer(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/IMqttPacketWriter.cs b/Source/MQTTnet/Formatter/IMqttPacketWriter.cs new file mode 100644 index 00000000..cce98cf5 --- /dev/null +++ b/Source/MQTTnet/Formatter/IMqttPacketWriter.cs @@ -0,0 +1,20 @@ +namespace MQTTnet.Formatter +{ + public interface IMqttPacketWriter + { + int Length { get; } + + void WriteWithLengthPrefix(string value); + void Write(byte returnCode); + void WriteWithLengthPrefix(byte[] payload); + void Write(ushort keepAlivePeriod); + + void Write(IMqttPacketWriter propertyWriter); + void WriteVariableLengthInteger(uint length); + void Write(byte[] payload, int v, int length); + void Reset(int v); + void Seek(int v); + void FreeBuffer(); + byte[] GetBuffer(); + } +} diff --git a/Source/MQTTnet/Serializer/MqttFixedHeader.cs b/Source/MQTTnet/Formatter/MqttFixedHeader.cs similarity index 53% rename from Source/MQTTnet/Serializer/MqttFixedHeader.cs rename to Source/MQTTnet/Formatter/MqttFixedHeader.cs index a8c2015b..4c693337 100644 --- a/Source/MQTTnet/Serializer/MqttFixedHeader.cs +++ b/Source/MQTTnet/Formatter/MqttFixedHeader.cs @@ -1,15 +1,18 @@ -namespace MQTTnet.Serializer +namespace MQTTnet.Formatter { public struct MqttFixedHeader { - public MqttFixedHeader(byte flags, int remainingLength) + 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; } } } diff --git a/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs b/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs new file mode 100644 index 00000000..c45fb3d9 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs @@ -0,0 +1,153 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using MQTTnet.Exceptions; + +namespace MQTTnet.Formatter +{ + public class MqttPacketBodyReader : IMqttPacketBodyReader + { + private readonly byte[] _buffer; + private readonly ulong _initialOffset; + private readonly ulong _length; + + public MqttPacketBodyReader(byte[] buffer, int offset, int length) + : this(buffer, (ulong)offset, (ulong)length) + { + } + + public MqttPacketBodyReader(byte[] buffer, ulong offset, ulong length) + { + _buffer = buffer; + _initialOffset = offset; + Offset = offset; + _length = length; + } + + public ulong Offset { get; private set; } + + public ulong Length => _length - Offset; + + public bool EndOfStream => Offset == _length; + + public void Seek(ulong position) + { + Offset = _initialOffset + position; + } + + public ArraySegment Read(uint length) + { + ValidateReceiveBuffer(length); + + var buffer = new ArraySegment(_buffer, (int)Offset, (int)length); + Offset += length; + return buffer; + } + + public byte ReadByte() + { + ValidateReceiveBuffer(1); + return _buffer[Offset++]; + } + + public bool ReadBoolean() + { + ValidateReceiveBuffer(1); + var buffer = _buffer[Offset++]; + + if (buffer == 0) + { + return false; + } + + if (buffer == 1) + { + return true; + } + + throw new MqttProtocolViolationException("Boolean values can be 0 or 1 only."); + } + + public byte[] ReadRemainingData() + { + return new ArraySegment(_buffer, (int)Offset, (int)(_length - Offset)).ToArray(); + } + + public ushort ReadTwoByteInteger() + { + ValidateReceiveBuffer(2); + + var msb = _buffer[Offset++]; + var lsb = _buffer[Offset++]; + + return (ushort)(msb << 8 | lsb); + } + + public uint ReadFourByteInteger() + { + ValidateReceiveBuffer(4); + + var byte0 = _buffer[Offset++]; + var byte1 = _buffer[Offset++]; + var byte2 = _buffer[Offset++]; + var byte3 = _buffer[Offset++]; + + return (uint)(byte0 << 24 | byte1 << 16 | byte2 << 8 | byte3); + } + + public byte[] ReadWithLengthPrefix() + { + return ReadSegmentWithLengthPrefix().ToArray(); + } + + private ArraySegment ReadSegmentWithLengthPrefix() + { + var length = ReadTwoByteInteger(); + + ValidateReceiveBuffer(length); + + var result = new ArraySegment(_buffer, (int)Offset, length); + Offset += length; + + return result; + } + + public uint ReadVariableLengthInteger() + { + var multiplier = 1; + var value = 0U; + byte encodedByte; + + do + { + encodedByte = ReadByte(); + value += (uint)((encodedByte & 127) * multiplier); + + if (multiplier > 2097152) + { + throw new MqttProtocolViolationException("Variable length integer is invalid."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateReceiveBuffer(uint length) + { + if (_length < Offset + length) + { + throw new ArgumentOutOfRangeException(nameof(_buffer), $"Expected at least {Offset + length} bytes but there are only {_length} bytes"); + } + } + + public string ReadStringWithLengthPrefix() + { + var buffer = ReadSegmentWithLengthPrefix(); + return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); + } + } +} diff --git a/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs new file mode 100644 index 00000000..723b6b8c --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs @@ -0,0 +1,166 @@ +using System; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Formatter.V3; +using MQTTnet.Formatter.V5; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public class MqttPacketFormatterAdapter + { + private IMqttPacketFormatter _formatter; + + public MqttPacketFormatterAdapter() + : this(new MqttPacketWriter()) + { + } + + public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion) + : this(protocolVersion, new MqttPacketWriter()) + { + } + + public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, IMqttPacketWriter writer) + : this(writer) + { + UseProtocolVersion(protocolVersion); + } + + public MqttPacketFormatterAdapter(IMqttPacketWriter writer) + { + Writer = writer; + } + + public MqttProtocolVersion ProtocolVersion { get; private set; } = MqttProtocolVersion.Unknown; + + public IMqttDataConverter DataConverter + { + get + { + ThrowIfFormatterNotSet(); + + return _formatter.DataConverter; + } + } + + public IMqttPacketWriter Writer { get; } + + public ArraySegment Encode(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + ThrowIfFormatterNotSet(); + + return _formatter.Encode(packet); + } + + public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); + + ThrowIfFormatterNotSet(); + + return _formatter.Decode(receivedMqttPacket); + } + + public void FreeBuffer() + { + _formatter?.FreeBuffer(); + } + + public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + var protocolVersion = ParseProtocolVersion(receivedMqttPacket); + + // Reset the position of the stream because the protocol version is part of + // the regular CONNECT packet. So it will not properly deserialized if this + // data is missing. + receivedMqttPacket.Body.Seek(0); + + UseProtocolVersion(protocolVersion); + } + + private void UseProtocolVersion(MqttProtocolVersion protocolVersion) + { + if (protocolVersion == MqttProtocolVersion.Unknown) + { + throw new InvalidOperationException("MQTT protocol version is invalid."); + } + + ProtocolVersion = protocolVersion; + _formatter = GetMqttPacketFormatter(protocolVersion, Writer); + } + + public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, IMqttPacketWriter writer) + { + if (protocolVersion == MqttProtocolVersion.Unknown) + { + throw new InvalidOperationException("MQTT protocol version is invalid."); + } + + switch (protocolVersion) + { + case MqttProtocolVersion.V500: + { + return new MqttV500PacketFormatter(writer); + } + case MqttProtocolVersion.V311: + { + return new MqttV311PacketFormatter(writer); + } + case MqttProtocolVersion.V310: + { + return new MqttV310PacketFormatter(writer); + } + default: + { + throw new NotSupportedException(); + } + } + } + + private MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); + + var protocolName = receivedMqttPacket.Body.ReadStringWithLengthPrefix(); + var protocolLevel = receivedMqttPacket.Body.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."); + } + + private void ThrowIfFormatterNotSet() + { + if (_formatter == null) + { + throw new InvalidOperationException("Protocol version not set or detected."); + } + } + } +} diff --git a/Source/MQTTnet/Formatter/MqttPacketReader.cs b/Source/MQTTnet/Formatter/MqttPacketReader.cs new file mode 100644 index 00000000..2589c5b2 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketReader.cs @@ -0,0 +1,169 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Exceptions; + +namespace MQTTnet.Formatter +{ + public class MqttPacketReader + { + private readonly byte[] _singleByteBuffer = new byte[1]; + + private readonly IMqttChannel _channel; + + public MqttPacketReader(IMqttChannel channel) + { + _channel = channel ?? throw new ArgumentNullException(nameof(channel)); + } + + public async Task ReadFixedHeaderAsync(byte[] fixedHeaderBuffer, 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. + // async/await is used here because the next packet is received in a couple of minutes so the performance + // impact is acceptable according to a useless waiting thread. + 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 null; + } + + if (bytesRead == 0) + { + return new ReadFixedHeaderResult + { + ConnectionClosed = true + }; + } + + totalBytesRead += bytesRead; + } + + var hasRemainingLength = buffer[1] != 0; + if (!hasRemainingLength) + { + return new ReadFixedHeaderResult + { + FixedHeader = new MqttFixedHeader(buffer[0], 0, totalBytesRead) + }; + } + +#if WINDOWS_UWP + // UWP will have a dead lock when calling this not async. + var bodyLength = await ReadBodyLengthAsync(buffer[1], cancellationToken).ConfigureAwait(false); +#else + // Here the async/await pattern is not used because the overhead of context switches + // is too big for reading 1 byte in a row. We expect that the remaining data was sent + // directly after the initial bytes. If the client disconnects just in this moment we + // will get an exception anyway. + var bodyLength = ReadBodyLength(buffer[1], cancellationToken); +#endif + + if (!bodyLength.HasValue) + { + return new ReadFixedHeaderResult + { + ConnectionClosed = true + }; + } + + totalBytesRead += bodyLength.Value; + return new ReadFixedHeaderResult + { + FixedHeader = new MqttFixedHeader(buffer[0], bodyLength.Value, totalBytesRead) + }; + } + +#if !WINDOWS_UWP + private int? ReadBodyLength(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 null; + } + + var readCount = _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).GetAwaiter().GetResult(); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (readCount == 0) + { + return null; + } + + encodedByte = _singleByteBuffer[0]; + + value += (encodedByte & 127) * multiplier; + multiplier *= 128; + } + + return value; + } +#else + + private 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 null; + } + + var readCount = await _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (readCount == 0) + { + return null; + } + + encodedByte = _singleByteBuffer[0]; + + value += (encodedByte & 127) * multiplier; + multiplier *= 128; + } + + return value; + } +#endif + } +} diff --git a/Source/MQTTnet/Formatter/MqttPacketWriter.cs b/Source/MQTTnet/Formatter/MqttPacketWriter.cs new file mode 100644 index 00000000..4c44d559 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketWriter.cs @@ -0,0 +1,240 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + /// + /// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. + /// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the + /// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal + /// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). + /// + public class MqttPacketWriter : IMqttPacketWriter + { + private static readonly ArraySegment ZeroVariableLengthIntegerArray = new ArraySegment(new byte[1], 0, 1); + private static readonly ArraySegment ZeroTwoByteIntegerArray = new ArraySegment(new byte[2], 0, 2); + + public static int InitialBufferSize = 128; + public static int MaxBufferSize = 4096; + + private byte[] _buffer = new byte[InitialBufferSize]; + + private int _offset; + + public int Length { get; private set; } + + public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) + { + var fixedHeader = (int)packetType << 4; + fixedHeader |= flags; + return (byte)fixedHeader; + } + + public static int GetLengthOfVariableInteger(uint value) + { + var result = 0; + var x = value; + do + { + x = x / 128; + result++; + } while (x > 0); + + return result; + } + + public static ArraySegment EncodeVariableLengthInteger(uint value) + { + if (value == 0) + { + return ZeroVariableLengthIntegerArray; + } + + if (value <= 127) + { + return new ArraySegment(new[] { (byte)value }, 0, 1); + } + + var buffer = new byte[4]; + var bufferOffset = 0; + + var x = value; + do + { + var encodedByte = x % 128; + x = x / 128; + if (x > 0) + { + encodedByte = encodedByte | 128; + } + + buffer[bufferOffset] = (byte)encodedByte; + bufferOffset++; + } while (x > 0); + + return new ArraySegment(buffer, 0, bufferOffset); + } + + public void WriteVariableLengthInteger(uint value) + { + Write(EncodeVariableLengthInteger(value)); + } + + public void WriteWithLengthPrefix(string value) + { + if (string.IsNullOrEmpty(value)) + { + Write(ZeroTwoByteIntegerArray); + } + else + { + WriteWithLengthPrefix(Encoding.UTF8.GetBytes(value)); + } + } + + public void WriteWithLengthPrefix(byte[] value) + { + if (value == null || value.Length == 0) + { + Write(ZeroTwoByteIntegerArray); + } + else + { + EnsureAdditionalCapacity(value.Length + 2); + Write((ushort)value.Length); + Write(value, 0, value.Length); + } + } + + public void Write(byte @byte) + { + EnsureAdditionalCapacity(1); + + _buffer[_offset] = @byte; + IncreasePosition(1); + } + + public void Write(ushort value) + { + EnsureAdditionalCapacity(2); + + _buffer[_offset] = (byte)(value >> 8); + IncreasePosition(1); + _buffer[_offset] = (byte)value; + IncreasePosition(1); + } + + public void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + + if (count == 0) + { + return; + } + + EnsureAdditionalCapacity(count); + + Array.Copy(buffer, offset, _buffer, _offset, count); + IncreasePosition(count); + } + + public void Write(IMqttPacketWriter propertyWriter) + { + if (propertyWriter == null) throw new ArgumentNullException(nameof(propertyWriter)); + + if (propertyWriter is MqttPacketWriter writer) + { + if (writer.Length == 0) + { + return; + } + + Write(writer._buffer, 0, writer.Length); + return; + } + + throw new InvalidOperationException($"{nameof(propertyWriter)} must be of type {typeof(MqttPacketWriter).Name}"); + } + + public void Reset(int length) + { + Length = length; + } + + public void Seek(int position) + { + EnsureCapacity(position); + _offset = position; + } + + public byte[] GetBuffer() + { + return _buffer; + } + + public void FreeBuffer() + { + // This method frees the used memory by shrinking the buffer. This is required because the buffer + // is used across several messages. In general this is not a big issue because subsequent Ping packages + // have the same size but a very big publish package with 100 MB of payload will increase the buffer + // a lot and the size will never reduced. So this method tries to find a size which can be held in + // memory for a long time without causing troubles. + + if (_buffer.Length < MaxBufferSize) + { + return; + } + + Array.Resize(ref _buffer, MaxBufferSize); + } + + private void Write(ArraySegment buffer) + { + Write(buffer.Array, buffer.Offset, buffer.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureAdditionalCapacity(int additionalCapacity) + { + var freeSpace = _buffer.Length - _offset; + if (freeSpace >= additionalCapacity) + { + return; + } + + EnsureCapacity(_buffer.Length + additionalCapacity - freeSpace); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) + { + var newBufferLength = _buffer.Length; + + if (newBufferLength >= capacity) + { + return; + } + + while (newBufferLength < capacity) + { + newBufferLength *= 2; + } + + Array.Resize(ref _buffer, newBufferLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void IncreasePosition(int length) + { + _offset += length; + + if (_offset > Length) + { + Length = _offset; + } + } + } +} diff --git a/Source/MQTTnet/Formatter/MqttProtocolVersion.cs b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs new file mode 100644 index 00000000..2121e7e5 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs @@ -0,0 +1,11 @@ +namespace MQTTnet.Formatter +{ + public enum MqttProtocolVersion + { + Unknown = 0, + + V310 = 3, + V311 = 4, + V500 = 5 + } +} diff --git a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs new file mode 100644 index 00000000..30ad73ec --- /dev/null +++ b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs @@ -0,0 +1,9 @@ +namespace MQTTnet.Formatter +{ + public class ReadFixedHeaderResult + { + public bool ConnectionClosed { get; set; } + + public MqttFixedHeader FixedHeader { get; set; } + } +} diff --git a/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs b/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs new file mode 100644 index 00000000..58b58c21 --- /dev/null +++ b/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs @@ -0,0 +1,224 @@ +using System; +using System.Linq; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V3 +{ + public class MqttV310DataConverter : IMqttDataConverter + { + public MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + if (applicationMessage.UserProperties?.Any() == true) + { + throw new MqttProtocolViolationException("User properties are not supported in MQTT version 3."); + } + + return new MqttPublishPacket + { + Topic = applicationMessage.Topic, + Payload = applicationMessage.Payload, + QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, + Retain = applicationMessage.Retain, + Dup = false + }; + } + + public MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket) + { + return new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubAckReasonCode.Success + }; + } + + public MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket) + { + if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); + + return new MqttApplicationMessage + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, + Retain = publishPacket.Retain + }; + } + + public MqttClientAuthenticateResult CreateClientConnectResult(MqttConnAckPacket connAckPacket) + { + if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); + + MqttClientConnectResultCode resultCode; + switch (connAckPacket.ReturnCode.Value) + { + case MqttConnectReturnCode.ConnectionAccepted: + { + resultCode = MqttClientConnectResultCode.Success; + break; + } + + case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: + { + resultCode = MqttClientConnectResultCode.UnsupportedProtocolVersion; + break; + } + + case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: + { + resultCode = MqttClientConnectResultCode.NotAuthorized; + break; + } + + case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: + { + resultCode = MqttClientConnectResultCode.BadUserNameOrPassword; + break; + } + + case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: + { + resultCode = MqttClientConnectResultCode.ClientIdentifierNotValid; + break; + } + + case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: + { + resultCode = MqttClientConnectResultCode.ServerUnavailable; + break; + } + + default: + throw new MqttProtocolViolationException("Received unexpected return code."); + } + + return new MqttClientAuthenticateResult + { + IsSessionPresent = connAckPacket.IsSessionPresent, + ResultCode = resultCode + }; + } + + public MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + return new MqttConnectPacket + { + ClientId = options.ClientId, + Username = options.Credentials?.Username, + Password = options.Credentials?.Password, + CleanSession = options.CleanSession, + KeepAlivePeriod = (ushort)options.KeepAlivePeriod.TotalSeconds, + WillMessage = willApplicationMessage + }; + } + + public MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) + { + if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); + if (subAckPacket == null) throw new ArgumentNullException(nameof(subAckPacket)); + + if (subAckPacket.ReturnCodes.Count != subscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + var result = new MqttClientSubscribeResult(); + + result.Items.AddRange(subscribePacket.TopicFilters.Select((t, i) => + new MqttClientSubscribeResultItem(t, (MqttClientSubscribeResultCode)subAckPacket.ReturnCodes[i]))); + + return result; + } + + public MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) + { + if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + if (unsubAckPacket == null) throw new ArgumentNullException(nameof(unsubAckPacket)); + + var result = new MqttClientUnsubscribeResult(); + + result.Items.AddRange(unsubscribePacket.TopicFilters.Select((t, i) => + new MqttClientUnsubscribeResultItem(t, MqttClientUnsubscribeResultCode.Success))); + + return result; + } + + public MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.UserProperties?.Any() == true) + { + throw new MqttProtocolViolationException("User properties are not supported in MQTT version 3."); + } + + var subscribePacket = new MqttSubscribePacket(); + subscribePacket.TopicFilters.AddRange(options.TopicFilters); + + return subscribePacket; + } + + public MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.UserProperties?.Any() == true) + { + throw new MqttProtocolViolationException("User properties are not supported in MQTT version 3."); + } + + var unsubscribePacket = new MqttUnsubscribePacket(); + unsubscribePacket.TopicFilters.AddRange(options.TopicFilters); + + return unsubscribePacket; + } + + public MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options) + { + if (options != null) + { + throw new MqttProtocolViolationException("Reason codes for disconnect are only supported for MQTTv5."); + } + + return new MqttDisconnectPacket(); + } + + public MqttClientPublishResult CreatePublishResult(MqttPubAckPacket pubAckPacket) + { + return new MqttClientPublishResult + { + PacketIdentifier = pubAckPacket?.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.Success + }; + } + + public MqttClientPublishResult CreatePublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) + { + if (pubRecPacket == null || pubCompPacket == null) + { + return new MqttClientPublishResult + { + ReasonCode = MqttClientPublishReasonCode.UnspecifiedError + }; + } + + return new MqttClientPublishResult + { + PacketIdentifier = pubCompPacket.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.Success + }; + } + } +} diff --git a/Source/MQTTnet/Serializer/MqttPacketSerializer.cs b/Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs similarity index 60% rename from Source/MQTTnet/Serializer/MqttPacketSerializer.cs rename to Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs index f1eb790a..70af10e6 100644 --- a/Source/MQTTnet/Serializer/MqttPacketSerializer.cs +++ b/Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs @@ -1,46 +1,59 @@ -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using System; +using System; using System.Linq; using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; -namespace MQTTnet.Serializer +namespace MQTTnet.Formatter.V3 { - public class MqttPacketSerializer : IMqttPacketSerializer + public class MqttV310PacketFormatter : IMqttPacketFormatter { private const int FixedHeaderSize = 1; + + private readonly IMqttPacketWriter _packetWriter; - private readonly MqttPacketWriter _packetWriter = new MqttPacketWriter(); + public MqttV310PacketFormatter() + : this(new MqttPacketWriter()) + { - public MqttProtocolVersion ProtocolVersion { get; set; } = MqttProtocolVersion.V311; + } - public ArraySegment Serialize(MqttBasePacket packet) + public MqttV310PacketFormatter(IMqttPacketWriter packetWriter) + { + _packetWriter = packetWriter; + } + + + public IMqttDataConverter DataConverter { get; } = new MqttV310DataConverter(); + + public ArraySegment Encode(MqttBasePacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) - _packetWriter.Reset(); + _packetWriter.Reset(5); _packetWriter.Seek(5); - var fixedHeader = SerializePacket(packet, _packetWriter); - var remainingLength = _packetWriter.Length - 5; + var fixedHeader = EncodePacket(packet, _packetWriter); + var remainingLength = (uint)(_packetWriter.Length - 5); - var remainingLengthBuffer = MqttPacketWriter.EncodeRemainingLength(remainingLength); + var remainingLengthSize = MqttPacketWriter.GetLengthOfVariableInteger(remainingLength); - var headerSize = FixedHeaderSize + remainingLengthBuffer.Count; + var headerSize = FixedHeaderSize + remainingLengthSize; var headerOffset = 5 - headerSize; - // Position cursor on correct offset on beginining of array (has leading 0x0) + // Position cursor on correct offset on beginning of array (has leading 0x0) _packetWriter.Seek(headerOffset); _packetWriter.Write(fixedHeader); - _packetWriter.Write(remainingLengthBuffer.Array, remainingLengthBuffer.Offset, remainingLengthBuffer.Count); + _packetWriter.WriteVariableLengthInteger(remainingLength); var buffer = _packetWriter.GetBuffer(); + return new ArraySegment(buffer, headerOffset, _packetWriter.Length - headerOffset); } - public MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket) + public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) { if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); @@ -52,20 +65,20 @@ namespace MQTTnet.Serializer switch ((MqttControlPacketType)controlPacketType) { - case MqttControlPacketType.Connect: return DeserializeConnect(receivedMqttPacket.Body); - case MqttControlPacketType.ConnAck: return DeserializeConnAck(receivedMqttPacket.Body); + case MqttControlPacketType.Connect: return DecodeConnectPacket(receivedMqttPacket.Body); + case MqttControlPacketType.ConnAck: return DecodeConnAckPacket(receivedMqttPacket.Body); case MqttControlPacketType.Disconnect: return new MqttDisconnectPacket(); - case MqttControlPacketType.Publish: return DeserializePublish(receivedMqttPacket); - case MqttControlPacketType.PubAck: return DeserializePubAck(receivedMqttPacket.Body); - case MqttControlPacketType.PubRec: return DeserializePubRec(receivedMqttPacket.Body); - case MqttControlPacketType.PubRel: return DeserializePubRel(receivedMqttPacket.Body); - case MqttControlPacketType.PubComp: return DeserializePubComp(receivedMqttPacket.Body); + 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 new MqttPingReqPacket(); case MqttControlPacketType.PingResp: return new MqttPingRespPacket(); - case MqttControlPacketType.Subscribe: return DeserializeSubscribe(receivedMqttPacket.Body); - case MqttControlPacketType.SubAck: return DeserializeSubAck(receivedMqttPacket.Body); - case MqttControlPacketType.Unsubscibe: return DeserializeUnsubscribe(receivedMqttPacket.Body); - case MqttControlPacketType.UnsubAck: return DeserializeUnsubAck(receivedMqttPacket.Body); + 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."); } @@ -76,85 +89,86 @@ namespace MQTTnet.Serializer _packetWriter.FreeBuffer(); } - private byte SerializePacket(MqttBasePacket packet, MqttPacketWriter packetWriter) + private byte EncodePacket(MqttBasePacket packet, IMqttPacketWriter packetWriter) { switch (packet) { - case MqttConnectPacket connectPacket: return Serialize(connectPacket, packetWriter); - case MqttConnAckPacket connAckPacket: return Serialize(connAckPacket, packetWriter); - case MqttDisconnectPacket _: return SerializeEmptyPacket(MqttControlPacketType.Disconnect); - case MqttPingReqPacket _: return SerializeEmptyPacket(MqttControlPacketType.PingReq); - case MqttPingRespPacket _: return SerializeEmptyPacket(MqttControlPacketType.PingResp); - case MqttPublishPacket publishPacket: return Serialize(publishPacket, packetWriter); - case MqttPubAckPacket pubAckPacket: return Serialize(pubAckPacket, packetWriter); - case MqttPubRecPacket pubRecPacket: return Serialize(pubRecPacket, packetWriter); - case MqttPubRelPacket pubRelPacket: return Serialize(pubRelPacket, packetWriter); - case MqttPubCompPacket pubCompPacket: return Serialize(pubCompPacket, packetWriter); - case MqttSubscribePacket subscribePacket: return Serialize(subscribePacket, packetWriter); - case MqttSubAckPacket subAckPacket: return Serialize(subAckPacket, packetWriter); - case MqttUnsubscribePacket unsubscribePacket: return Serialize(unsubscribePacket, packetWriter); - case MqttUnsubAckPacket unsubAckPacket: return Serialize(unsubAckPacket, packetWriter); + case MqttConnectPacket connectPacket: return EncodeConnectPacket(connectPacket, packetWriter); + case MqttConnAckPacket connAckPacket: return EncodeConnAckPacket(connAckPacket, packetWriter); + case MqttDisconnectPacket _: return EncodeEmptyPacket(MqttControlPacketType.Disconnect); + case MqttPingReqPacket _: return EncodeEmptyPacket(MqttControlPacketType.PingReq); + case MqttPingRespPacket _: return EncodeEmptyPacket(MqttControlPacketType.PingResp); + case MqttPublishPacket publishPacket: return EncodePublishPacket(publishPacket, packetWriter); + case MqttPubAckPacket pubAckPacket: return EncodePubAckPacket(pubAckPacket, packetWriter); + case MqttPubRecPacket pubRecPacket: return EncodePubRecPacket(pubRecPacket, packetWriter); + case MqttPubRelPacket pubRelPacket: return EncodePubRelPacket(pubRelPacket, packetWriter); + case MqttPubCompPacket pubCompPacket: return EncodePubCompPacket(pubCompPacket, packetWriter); + case MqttSubscribePacket subscribePacket: return EncodeSubscribePacket(subscribePacket, packetWriter); + case MqttSubAckPacket subAckPacket: return EncodeSubAckPacket(subAckPacket, packetWriter); + case MqttUnsubscribePacket unsubscribePacket: return EncodeUnsubscribePacket(unsubscribePacket, packetWriter); + case MqttUnsubAckPacket unsubAckPacket: return EncodeUnsubAckPacket(unsubAckPacket, packetWriter); + default: throw new MqttProtocolViolationException("Packet type invalid."); } } - private static MqttBasePacket DeserializeUnsubAck(MqttPacketBodyReader body) + private static MqttBasePacket DecodeUnsubAckPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); return new MqttUnsubAckPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; } - private static MqttBasePacket DeserializePubComp(MqttPacketBodyReader body) + private static MqttBasePacket DecodePubCompPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); return new MqttPubCompPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; } - private static MqttBasePacket DeserializePubRel(MqttPacketBodyReader body) + private static MqttBasePacket DecodePubRelPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); return new MqttPubRelPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; } - private static MqttBasePacket DeserializePubRec(MqttPacketBodyReader body) + private static MqttBasePacket DecodePubRecPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); return new MqttPubRecPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; } - private static MqttBasePacket DeserializePubAck(MqttPacketBodyReader body) + private static MqttBasePacket DecodePubAckPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); return new MqttPubAckPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; } - private static MqttBasePacket DeserializeUnsubscribe(MqttPacketBodyReader body) + private static MqttBasePacket DecodeUnsubscribePacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); var packet = new MqttUnsubscribePacket { - PacketIdentifier = body.ReadUInt16(), + PacketIdentifier = body.ReadTwoByteInteger(), }; while (!body.EndOfStream) @@ -165,26 +179,30 @@ namespace MQTTnet.Serializer return packet; } - private static MqttBasePacket DeserializeSubscribe(MqttPacketBodyReader body) + private static MqttBasePacket DecodeSubscribePacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); var packet = new MqttSubscribePacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; while (!body.EndOfStream) { - packet.TopicFilters.Add(new TopicFilter( - body.ReadStringWithLengthPrefix(), - (MqttQualityOfServiceLevel)body.ReadByte())); + var topicFilter = new TopicFilter + { + Topic = body.ReadStringWithLengthPrefix(), + QualityOfServiceLevel = (MqttQualityOfServiceLevel)body.ReadByte() + }; + + packet.TopicFilters.Add(topicFilter); } return packet; } - private static MqttBasePacket DeserializePublish(ReceivedMqttPacket receivedMqttPacket) + private static MqttBasePacket DecodePublishPacket(ReceivedMqttPacket receivedMqttPacket) { ThrowIfBodyIsEmpty(receivedMqttPacket.Body); @@ -197,7 +215,7 @@ namespace MQTTnet.Serializer ushort? packetIdentifier = null; if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) { - packetIdentifier = receivedMqttPacket.Body.ReadUInt16(); + packetIdentifier = receivedMqttPacket.Body.ReadTwoByteInteger(); } var packet = new MqttPublishPacket @@ -205,45 +223,36 @@ namespace MQTTnet.Serializer PacketIdentifier = packetIdentifier, Retain = retain, Topic = topic, - Payload = receivedMqttPacket.Body.ReadRemainingData().ToArray(), QualityOfServiceLevel = qualityOfServiceLevel, Dup = dup }; + if (!receivedMqttPacket.Body.EndOfStream) + { + packet.Payload = receivedMqttPacket.Body.ReadRemainingData().ToArray(); + } + return packet; } - private static MqttBasePacket DeserializeConnect(MqttPacketBodyReader body) + private MqttBasePacket DecodeConnectPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); var protocolName = body.ReadStringWithLengthPrefix(); + var protocolVersion = body.ReadByte(); - MqttProtocolVersion protocolVersion; - if (protocolName == "MQTT") + if (protocolName != "MQTT" && protocolName != "MQIsdp") { - var protocolLevel = body.ReadByte(); - if (protocolLevel != 4) - { - throw new MqttProtocolViolationException($"Protocol level ({protocolLevel}) not supported for MQTT 3.1.1."); - } + throw new MqttProtocolViolationException("MQTT protocol name do not match MQTT v3."); + } - protocolVersion = MqttProtocolVersion.V311; - } - else if (protocolName == "MQIsdp") + if (protocolVersion != 3 && protocolVersion != 4) { - var protocolLevel = body.ReadByte(); - if (protocolLevel != 3) - { - throw new MqttProtocolViolationException($"Protocol level ({protocolLevel}) not supported for MQTT 3.1."); - } + throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3."); + } - protocolVersion = MqttProtocolVersion.V310; - } - else - { - throw new MqttProtocolViolationException($"Protocol name ({protocolName}) is not supported."); - } + var packet = new MqttConnectPacket(); var connectFlags = body.ReadByte(); if ((connectFlags & 0x1) > 0) @@ -251,11 +260,7 @@ namespace MQTTnet.Serializer throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); } - var packet = new MqttConnectPacket - { - ProtocolVersion = protocolVersion, - CleanSession = (connectFlags & 0x2) > 0 - }; + packet.CleanSession = (connectFlags & 0x2) > 0; var willFlag = (connectFlags & 0x4) > 0; var willQoS = (connectFlags & 0x18) >> 3; @@ -263,7 +268,7 @@ namespace MQTTnet.Serializer var passwordFlag = (connectFlags & 0x40) > 0; var usernameFlag = (connectFlags & 0x80) > 0; - packet.KeepAlivePeriod = body.ReadUInt16(); + packet.KeepAlivePeriod = body.ReadTwoByteInteger(); packet.ClientId = body.ReadStringWithLengthPrefix(); if (willFlag) @@ -271,7 +276,7 @@ namespace MQTTnet.Serializer packet.WillMessage = new MqttApplicationMessage { Topic = body.ReadStringWithLengthPrefix(), - Payload = body.ReadWithLengthPrefix().ToArray(), + Payload = body.ReadWithLengthPrefix(), QualityOfServiceLevel = (MqttQualityOfServiceLevel)willQoS, Retain = willRetain }; @@ -291,42 +296,36 @@ namespace MQTTnet.Serializer return packet; } - private static MqttBasePacket DeserializeSubAck(MqttPacketBodyReader body) + private static MqttBasePacket DecodeSubAckPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); var packet = new MqttSubAckPacket { - PacketIdentifier = body.ReadUInt16() + PacketIdentifier = body.ReadTwoByteInteger() }; while (!body.EndOfStream) { - packet.SubscribeReturnCodes.Add((MqttSubscribeReturnCode)body.ReadByte()); + packet.ReturnCodes.Add((MqttSubscribeReturnCode)body.ReadByte()); } return packet; } - private MqttBasePacket DeserializeConnAck(MqttPacketBodyReader body) + protected virtual MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) { ThrowIfBodyIsEmpty(body); var packet = new MqttConnAckPacket(); - var acknowledgeFlags = body.ReadByte(); - - if (ProtocolVersion == MqttProtocolVersion.V311) - { - packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; - } - - packet.ConnectReturnCode = (MqttConnectReturnCode)body.ReadByte(); + body.ReadByte(); // Reserved. + packet.ReturnCode = (MqttConnectReturnCode)body.ReadByte(); return packet; } - private static void ValidateConnectPacket(MqttConnectPacket packet) + protected void ValidateConnectPacket(MqttConnectPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); @@ -345,21 +344,12 @@ namespace MQTTnet.Serializer } } - private byte Serialize(MqttConnectPacket packet, MqttPacketWriter packetWriter) + protected virtual byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) { ValidateConnectPacket(packet); - // Write variable header - if (ProtocolVersion == MqttProtocolVersion.V311) - { - packetWriter.WriteWithLengthPrefix("MQTT"); - packetWriter.Write(4); // 3.1.2.2 Protocol Level 4 - } - else - { - packetWriter.WriteWithLengthPrefix("MQIsdp"); - packetWriter.Write(3); // Protocol Level 3 - } + packetWriter.WriteWithLengthPrefix("MQIsdp"); + packetWriter.Write(3); // Protocol Level 3 byte connectFlags = 0x0; if (packet.CleanSession) @@ -416,33 +406,15 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); } - private byte Serialize(MqttConnAckPacket packet, MqttPacketWriter packetWriter) + protected virtual byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) { - if (ProtocolVersion == MqttProtocolVersion.V310) - { - packetWriter.Write(0); - } - else if (ProtocolVersion == MqttProtocolVersion.V311) - { - byte connectAcknowledgeFlags = 0x0; - if (packet.IsSessionPresent) - { - connectAcknowledgeFlags |= 0x1; - } - - packetWriter.Write(connectAcknowledgeFlags); - } - else - { - throw new MqttProtocolViolationException("Protocol version not supported."); - } - - packetWriter.Write((byte)packet.ConnectReturnCode); + packetWriter.Write(0); // Reserved. + packetWriter.Write((byte)packet.ReturnCode.Value); return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); } - private static byte Serialize(MqttPubRelPacket packet, MqttPacketWriter packetWriter) + private static byte EncodePubRelPacket(MqttPubRelPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -454,7 +426,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); } - private static byte Serialize(MqttPublishPacket packet, MqttPacketWriter packetWriter) + private static byte EncodePublishPacket(MqttPublishPacket packet, IMqttPacketWriter packetWriter) { ValidatePublishPacket(packet); @@ -499,7 +471,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); } - private static byte Serialize(MqttPubAckPacket packet, MqttPacketWriter packetWriter) + private static byte EncodePubAckPacket(MqttPubAckPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -511,7 +483,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); } - private static byte Serialize(MqttPubRecPacket packet, MqttPacketWriter packetWriter) + private static byte EncodePubRecPacket(MqttPubRecPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -523,7 +495,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); } - private static byte Serialize(MqttPubCompPacket packet, MqttPacketWriter packetWriter) + private static byte EncodePubCompPacket(MqttPubCompPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -535,7 +507,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); } - private static byte Serialize(MqttSubscribePacket packet, MqttPacketWriter packetWriter) + private static byte EncodeSubscribePacket(MqttSubscribePacket packet, IMqttPacketWriter packetWriter) { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); @@ -558,7 +530,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); } - private static byte Serialize(MqttSubAckPacket packet, MqttPacketWriter packetWriter) + private static byte EncodeSubAckPacket(MqttSubAckPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -567,9 +539,9 @@ namespace MQTTnet.Serializer packetWriter.Write(packet.PacketIdentifier.Value); - if (packet.SubscribeReturnCodes?.Any() == true) + if (packet.ReturnCodes?.Any() == true) { - foreach (var packetSubscribeReturnCode in packet.SubscribeReturnCodes) + foreach (var packetSubscribeReturnCode in packet.ReturnCodes) { packetWriter.Write((byte)packetSubscribeReturnCode); } @@ -578,7 +550,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.SubAck); } - private static byte Serialize(MqttUnsubscribePacket packet, MqttPacketWriter packetWriter) + private static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, IMqttPacketWriter packetWriter) { if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); @@ -600,7 +572,7 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); } - private static byte Serialize(MqttUnsubAckPacket packet, MqttPacketWriter packetWriter) + private static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, IMqttPacketWriter packetWriter) { if (!packet.PacketIdentifier.HasValue) { @@ -611,13 +583,13 @@ namespace MQTTnet.Serializer return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); } - private static byte SerializeEmptyPacket(MqttControlPacketType type) + private static byte EncodeEmptyPacket(MqttControlPacketType type) { return MqttPacketWriter.BuildFixedHeader(type); } // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - private static void ThrowIfBodyIsEmpty(MqttPacketBodyReader body) + protected static void ThrowIfBodyIsEmpty(IMqttPacketBodyReader body) { if (body == null || body.Length == 0) { diff --git a/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs b/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs new file mode 100644 index 00000000..a846cd09 --- /dev/null +++ b/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs @@ -0,0 +1,109 @@ +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V3 +{ + public class MqttV311PacketFormatter : MqttV310PacketFormatter + { + public MqttV311PacketFormatter() + : base() + { + } + + public MqttV311PacketFormatter(IMqttPacketWriter packetWriter) + : base(packetWriter) + { + } + + protected override byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) + { + ValidateConnectPacket(packet); + + packetWriter.WriteWithLengthPrefix("MQTT"); + packetWriter.Write(4); // 3.1.2.2 Protocol Level 4 + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillMessage != null) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); + + if (packet.WillMessage.Retain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + packetWriter.Write(connectFlags); + packetWriter.Write(packet.KeepAlivePeriod); + packetWriter.WriteWithLengthPrefix(packet.ClientId); + + if (packet.WillMessage != null) + { + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); + } + + if (packet.Username != null) + { + packetWriter.WriteWithLengthPrefix(packet.Username); + } + + if (packet.Password != null) + { + packetWriter.WriteWithLengthPrefix(packet.Password); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + protected override byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) + { + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + packetWriter.Write(connectAcknowledgeFlags); + packetWriter.Write((byte)packet.ReturnCode); + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + protected override MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttConnAckPacket(); + + var acknowledgeFlags = body.ReadByte(); + + packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; + packet.ReturnCode = (MqttConnectReturnCode)body.ReadByte(); + + return packet; + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs b/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs new file mode 100644 index 00000000..8071fc32 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500DataConverter : IMqttDataConverter + { + public MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + var packet = new MqttPublishPacket + { + Topic = applicationMessage.Topic, + Payload = applicationMessage.Payload, + QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, + Retain = applicationMessage.Retain, + Dup = false, + Properties = new MqttPublishPacketProperties + { + ContentType = applicationMessage.ContentType, + CorrelationData = applicationMessage.CorrelationData, + MessageExpiryInterval = applicationMessage.MessageExpiryInterval, + PayloadFormatIndicator = applicationMessage.PayloadFormatIndicator, + ResponseTopic = applicationMessage.ResponseTopic, + SubscriptionIdentifier = applicationMessage.SubscriptionIdentifier, + TopicAlias = applicationMessage.TopicAlias + } + }; + + if (applicationMessage.UserProperties != null) + { + packet.Properties.UserProperties.AddRange(applicationMessage.UserProperties); + } + + return packet; + } + + public MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket) + { + return new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubAckReasonCode.Success + }; + } + + public MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket) + { + return new MqttApplicationMessage + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, + Retain = publishPacket.Retain, + UserProperties = publishPacket.Properties?.UserProperties ?? new List() + }; + } + + public MqttClientAuthenticateResult CreateClientConnectResult(MqttConnAckPacket connAckPacket) + { + if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); + + return new MqttClientAuthenticateResult + { + IsSessionPresent = connAckPacket.IsSessionPresent, + ResultCode = (MqttClientConnectResultCode)connAckPacket.ReasonCode.Value + }; + } + + public MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + return new MqttConnectPacket + { + ClientId = options.ClientId, + Username = options.Credentials?.Username, + Password = options.Credentials?.Password, + CleanSession = options.CleanSession, + KeepAlivePeriod = (ushort)options.KeepAlivePeriod.TotalSeconds, + WillMessage = willApplicationMessage, + Properties = new MqttConnectPacketProperties + { + AuthenticationMethod = options.AuthenticationMethod, + AuthenticationData = options.AuthenticationData, + WillDelayInterval = options.WillDelayInterval, + MaximumPacketSize = options.MaximumPacketSize, + ReceiveMaximum = options.ReceiveMaximum, + RequestProblemInformation = options.RequestProblemInformation, + RequestResponseInformation = options.RequestResponseInformation, + SessionExpiryInterval = options.SessionExpiryInterval, + TopicAliasMaximum = options.TopicAliasMaximum + } + }; + } + + public MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) + { + if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); + if (subAckPacket == null) throw new ArgumentNullException(nameof(subAckPacket)); + + if (subAckPacket.ReasonCodes.Count != subscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException("The reason codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + var result = new MqttClientSubscribeResult(); + + result.Items.AddRange(subscribePacket.TopicFilters.Select((t, i) => + new MqttClientSubscribeResultItem(t, (MqttClientSubscribeResultCode)subAckPacket.ReasonCodes[i]))); + + return result; + } + + public MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) + { + if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + if (unsubAckPacket == null) throw new ArgumentNullException(nameof(unsubAckPacket)); + + if (unsubAckPacket.ReasonCodes.Count != unsubscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + var result = new MqttClientUnsubscribeResult(); + + result.Items.AddRange(unsubscribePacket.TopicFilters.Select((t, i) => + new MqttClientUnsubscribeResultItem(t, (MqttClientUnsubscribeResultCode)unsubAckPacket.ReasonCodes[i]))); + + return result; + } + + public MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + var packet = new MqttSubscribePacket + { + Properties = new MqttSubscribePacketProperties() + }; + + packet.TopicFilters.AddRange(options.TopicFilters); + packet.Properties.UserProperties.AddRange(options.UserProperties); + + return packet; + } + + public MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + var packet = new MqttUnsubscribePacket + { + Properties = new MqttUnsubscribePacketProperties() + }; + + packet.TopicFilters.AddRange(options.TopicFilters); + packet.Properties.UserProperties.AddRange(options.UserProperties); + + return packet; + } + + public MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options) + { + var packet = new MqttDisconnectPacket(); + + if (options == null) + { + packet.ReasonCode = MqttDisconnectReasonCode.NormalDisconnection; + } + else + { + packet.ReasonCode = (MqttDisconnectReasonCode)options.ReasonCode; + } + + return packet; + } + + public MqttClientPublishResult CreatePublishResult(MqttPubAckPacket pubAckPacket) + { + var result = new MqttClientPublishResult + { + ReasonCode = MqttClientPublishReasonCode.Success, + ReasonString = pubAckPacket?.Properties?.ReasonString, + UserProperties = pubAckPacket?.Properties?.UserProperties + }; + + if (pubAckPacket != null) + { + // QoS 0 has no response. So we treat it as a success always. + // Both enums have the same values. So it can be easily converted. + result.ReasonCode = (MqttClientPublishReasonCode)pubAckPacket.ReasonCode; + + result.PacketIdentifier = pubAckPacket.PacketIdentifier; + } + + return result; + } + + public MqttClientPublishResult CreatePublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) + { + if (pubRecPacket == null || pubCompPacket == null) + { + return new MqttClientPublishResult + { + ReasonCode = MqttClientPublishReasonCode.UnspecifiedError + }; + } + + // The PUBCOMP is the last packet in QoS 2. So we use the results from that instead of PUBREC. + if (pubCompPacket.ReasonCode == MqttPubCompReasonCode.PacketIdentifierNotFound) + { + return new MqttClientPublishResult + { + PacketIdentifier = pubCompPacket.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.UnspecifiedError, + ReasonString = pubCompPacket.Properties?.ReasonString, + UserProperties = pubCompPacket.Properties?.UserProperties + }; + } + + var result = new MqttClientPublishResult + { + PacketIdentifier = pubCompPacket.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.Success, + ReasonString = pubCompPacket.Properties?.ReasonString, + UserProperties = pubCompPacket.Properties?.UserProperties + }; + + if (pubRecPacket.ReasonCode.HasValue) + { + // Both enums share the same values. + result.ReasonCode = (MqttClientPublishReasonCode)pubRecPacket.ReasonCode.Value; + } + + return result; + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs new file mode 100644 index 00000000..46692cca --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs @@ -0,0 +1,720 @@ +using System; +using System.Linq; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500PacketDecoder + { + public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); + + var controlPacketType = receivedMqttPacket.FixedHeader >> 4; + if (controlPacketType < 1 || controlPacketType > 15) + { + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + } + + switch ((MqttControlPacketType)controlPacketType) + { + case MqttControlPacketType.Connect: return DecodeConnectPacket(receivedMqttPacket.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 DecodePingReqPacket(); + case MqttControlPacketType.PingResp: return DecodePingRespPacket(); + 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."); + } + } + + private static MqttBasePacket DecodeConnectPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttConnectPacket + { + Properties = new MqttConnectPacketProperties() + }; + + var protocolName = body.ReadStringWithLengthPrefix(); + var protocolVersion = body.ReadByte(); + + if (protocolName != "MQTT" && protocolVersion != 5) + { + throw new MqttProtocolViolationException("MQTT protocol name and version do not match MQTT v5."); + } + + var connectFlags = body.ReadByte(); + + var cleanSessionFlag = (connectFlags & 0x02) > 0; + var willMessageFlag = (connectFlags & 0x04) > 0; + var willMessageQoS = (byte)(connectFlags >> 3 & 3); + var willMessageRetainFlag = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; + + packet.CleanSession = cleanSessionFlag; + + if (willMessageFlag) + { + packet.WillMessage = new MqttApplicationMessage + { + QualityOfServiceLevel = (MqttQualityOfServiceLevel)willMessageQoS, + Retain = willMessageRetainFlag + }; + } + + packet.KeepAlivePeriod = body.ReadTwoByteInteger(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WillDelayInterval) + { + packet.Properties.WillDelayInterval = propertiesReader.ReadWillDelayInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.Properties.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.Properties.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.Properties.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestResponseInformation) + { + packet.Properties.RequestResponseInformation = propertiesReader.RequestResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestProblemInformation) + { + packet.Properties.RequestProblemInformation = propertiesReader.RequestProblemInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); + } + } + + packet.ClientId = body.ReadStringWithLengthPrefix(); + + if (packet.WillMessage != null) + { + packet.WillMessage.Topic = body.ReadStringWithLengthPrefix(); + packet.WillMessage.Payload = body.ReadWithLengthPrefix(); + } + + if (usernameFlag) + { + packet.Username = body.ReadStringWithLengthPrefix(); + } + + if (passwordFlag) + { + packet.Password = body.ReadStringWithLengthPrefix(); + } + + return packet; + } + + private static MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var acknowledgeFlags = body.ReadByte(); + + var packet = new MqttConnAckPacket + { + IsSessionPresent = (acknowledgeFlags & 0x1) > 0, + ReasonCode = (MqttConnectReasonCode)body.ReadByte(), + Properties = new MqttConnAckPacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RetainAvailable) + { + packet.Properties.RetainAvailable = propertiesReader.ReadRetainAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.Properties.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AssignedClientIdentifer) + { + packet.Properties.AssignedClientIdentifier = propertiesReader.ReadAssignedClientIdentifier(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.Properties.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.Properties.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WildcardSubscriptionAvailable) + { + packet.Properties.WildcardSubscriptionAvailable = propertiesReader.ReadWildcardSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifiersAvailable) + { + packet.Properties.SubscriptionIdentifiersAvailable = propertiesReader.ReadSubscriptionIdentifiersAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SharedSubscriptionAvailable) + { + packet.Properties.SharedSubscriptionAvailable = propertiesReader.ReadSharedSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerKeepAlive) + { + packet.Properties.ServerKeepAlive = propertiesReader.ReadServerKeepAlive(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseInformation) + { + packet.Properties.ResponseInformation = propertiesReader.ReadResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.Properties.ServerReference = propertiesReader.ReadServerReference(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodeDisconnectPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttDisconnectPacket + { + ReasonCode = (MqttDisconnectReasonCode)body.ReadByte(), + Properties = new MqttDisconnectPacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.Properties.ServerReference = propertiesReader.ReadServerReference(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttDisconnectPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodeSubscribePacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttSubscribePacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttSubscribePacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) + { + packet.Properties.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubscribePacket)); + } + } + + while (!body.EndOfStream) + { + var topic = body.ReadStringWithLengthPrefix(); + var options = body.ReadByte(); + + var qos = (MqttQualityOfServiceLevel)(options & 3); + var noLocal = (options & (1 << 2)) > 0; + var retainAsPublished = (options & (1 << 3)) > 0; + var retainHandling = (MqttRetainHandling)((options >> 4) & 3); + + packet.TopicFilters.Add(new TopicFilter + { + Topic = topic, + QualityOfServiceLevel = qos, + NoLocal = noLocal, + RetainAsPublished = retainAsPublished, + RetainHandling = retainHandling + }); + } + + return packet; + } + + private static MqttBasePacket DecodeSubAckPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttSubAckPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttSubAckPacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubAckPacket)); + } + } + + while (!body.EndOfStream) + { + var reasonCode = (MqttSubscribeReasonCode)body.ReadByte(); + packet.ReasonCodes.Add(reasonCode); + } + + return packet; + } + + private static MqttBasePacket DecodeUnsubscribePacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttUnsubscribePacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttUnsubscribePacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubscribePacket)); + } + } + + while (!body.EndOfStream) + { + packet.TopicFilters.Add(body.ReadStringWithLengthPrefix()); + } + + return packet; + } + + private static MqttBasePacket DecodeUnsubAckPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttUnsubAckPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttUnsubAckPacketProperties() + }; + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubAckPacket)); + } + } + + while (!body.EndOfStream) + { + var reasonCode = (MqttUnsubscribeReasonCode)body.ReadByte(); + packet.ReasonCodes.Add(reasonCode); + } + + return packet; + } + + private static MqttBasePacket DecodePingReqPacket() + { + return new MqttPingReqPacket(); + } + + private static MqttBasePacket DecodePingRespPacket() + { + return new MqttPingRespPacket(); + } + + private static MqttBasePacket DecodePublishPacket(byte header, IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var retain = (header & 1) > 0; + var qos = (MqttQualityOfServiceLevel)(header >> 1 & 3); + var dup = (header >> 3 & 1) > 0; + + var packet = new MqttPublishPacket + { + Topic = body.ReadStringWithLengthPrefix(), + Retain = retain, + QualityOfServiceLevel = qos, + Dup = dup, + Properties = new MqttPublishPacketProperties() + }; + + if (qos > 0) + { + packet.PacketIdentifier = body.ReadTwoByteInteger(); + } + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) + { + packet.Properties.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) + { + packet.Properties.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) + { + packet.Properties.TopicAlias = propertiesReader.ReadTopicAlias(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) + { + packet.Properties.ResponseTopic = propertiesReader.ReadResponseTopic(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) + { + packet.Properties.CorrelationData = propertiesReader.ReadCorrelationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) + { + packet.Properties.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) + { + packet.Properties.ContentType = propertiesReader.ReadContentType(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); + } + } + + if (!body.EndOfStream) + { + packet.Payload = body.ReadRemainingData().ToArray(); + } + + return packet; + } + + private static MqttBasePacket DecodePubAckPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttPubAckPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttPubAckPacketProperties() + }; + + if (body.EndOfStream) + { + packet.ReasonCode = MqttPubAckReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubAckReasonCode)body.ReadByte(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubAckPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodePubRecPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttPubRecPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttPubRecPacketProperties() + }; + + if (body.EndOfStream) + { + packet.ReasonCode = MqttPubRecReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubRecReasonCode)body.ReadByte(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRecPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodePubRelPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttPubRelPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttPubRelPacketProperties() + }; + + if (body.EndOfStream) + { + packet.ReasonCode = MqttPubRelReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubRelReasonCode)body.ReadByte(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRelPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodePubCompPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttPubCompPacket + { + PacketIdentifier = body.ReadTwoByteInteger(), + Properties = new MqttPubCompPacketProperties() + }; + + if (body.EndOfStream) + { + packet.ReasonCode = MqttPubCompReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubCompReasonCode)body.ReadByte(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubCompPacket)); + } + } + + return packet; + } + + private static MqttBasePacket DecodeAuthPacket(IMqttPacketBodyReader body) + { + ThrowIfBodyIsEmpty(body); + + var packet = new MqttAuthPacket + { + Properties = new MqttAuthPacketProperties() + }; + + if (body.EndOfStream) + { + packet.ReasonCode = MqttAuthenticateReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttAuthenticateReasonCode)body.ReadByte(); + + var propertiesReader = new MqttV500PropertiesReader(body); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.Properties.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) + { + propertiesReader.FillUserProperties(packet.Properties.UserProperties); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttAuthPacket)); + } + } + + return packet; + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private static void ThrowIfBodyIsEmpty(IMqttPacketBodyReader body) + { + if (body == null || body.Length == 0) + { + throw new MqttProtocolViolationException("Data from the body is required but not present."); + } + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs new file mode 100644 index 00000000..5800d30f --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs @@ -0,0 +1,578 @@ +using System; +using System.Linq; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500PacketEncoder + { + private readonly IMqttPacketWriter _packetWriter; + + public MqttV500PacketEncoder() + : this(new MqttPacketWriter()) + { + + } + + public MqttV500PacketEncoder(IMqttPacketWriter packetWriter) + { + _packetWriter = packetWriter; + } + + + public ArraySegment Encode(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) + _packetWriter.Reset(5); + _packetWriter.Seek(5); + + var fixedHeader = EncodePacket(packet, _packetWriter); + var remainingLength = (uint)(_packetWriter.Length - 5); + + var remainingLengthSize = MqttPacketWriter.GetLengthOfVariableInteger(remainingLength); + + var headerSize = 1 + remainingLengthSize; + var headerOffset = 5 - headerSize; + + // Position cursor on correct offset on beginning of array (has leading 0x0) + _packetWriter.Seek(headerOffset); + _packetWriter.Write(fixedHeader); + _packetWriter.WriteVariableLengthInteger(remainingLength); + + var buffer = _packetWriter.GetBuffer(); + return new ArraySegment(buffer, headerOffset, _packetWriter.Length - headerOffset); + } + + public void FreeBuffer() + { + _packetWriter.FreeBuffer(); + } + + private static byte EncodePacket(MqttBasePacket packet, IMqttPacketWriter packetWriter) + { + switch (packet) + { + case MqttConnectPacket connectPacket: return EncodeConnectPacket(connectPacket, packetWriter); + case MqttConnAckPacket connAckPacket: return EncodeConnAckPacket(connAckPacket, packetWriter); + case MqttDisconnectPacket disconnectPacket: return EncodeDisconnectPacket(disconnectPacket, packetWriter); + case MqttPingReqPacket _: return EncodePingReqPacket(); + case MqttPingRespPacket _: return EncodePingRespPacket(); + case MqttPublishPacket publishPacket: return EncodePublishPacket(publishPacket, packetWriter); + case MqttPubAckPacket pubAckPacket: return EncodePubAckPacket(pubAckPacket, packetWriter); + case MqttPubRecPacket pubRecPacket: return EncodePubRecPacket(pubRecPacket, packetWriter); + case MqttPubRelPacket pubRelPacket: return EncodePubRelPacket(pubRelPacket, packetWriter); + case MqttPubCompPacket pubCompPacket: return EncodePubCompPacket(pubCompPacket, packetWriter); + case MqttSubscribePacket subscribePacket: return EncodeSubscribePacket(subscribePacket, packetWriter); + case MqttSubAckPacket subAckPacket: return EncodeSubAckPacket(subAckPacket, packetWriter); + case MqttUnsubscribePacket unsubscribePacket: return EncodeUnsubscribePacket(unsubscribePacket, packetWriter); + case MqttUnsubAckPacket unsubAckPacket: return EncodeUnsubAckPacket(unsubAckPacket, packetWriter); + case MqttAuthPacket authPacket: return EncodeAuthPacket(authPacket, packetWriter); + + default: throw new MqttProtocolViolationException("Packet type invalid."); + } + } + + private static byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); + + if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) + { + throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); + } + + packetWriter.WriteWithLengthPrefix("MQTT"); + packetWriter.Write(5); // [3.1.2.2 Protocol Version] + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillMessage != null) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); + + if (packet.WillMessage.Retain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + packetWriter.Write(connectFlags); + packetWriter.Write(packet.KeepAlivePeriod); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteWillDelayInterval(packet.Properties.WillDelayInterval); + propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); + propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); + propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); + propertiesWriter.WriteRequestProblemInformation(packet.Properties.RequestProblemInformation); + propertiesWriter.WriteRequestResponseInformation(packet.Properties.RequestResponseInformation); + propertiesWriter.WriteReceiveMaximum(packet.Properties.ReceiveMaximum); + propertiesWriter.WriteTopicAlias(packet.Properties.TopicAliasMaximum); + propertiesWriter.WriteMaximumPacketSize(packet.Properties.MaximumPacketSize); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + packetWriter.WriteWithLengthPrefix(packet.ClientId); + + if (packet.WillMessage != null) + { + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); + packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); + } + + if (packet.Username != null) + { + packetWriter.WriteWithLengthPrefix(packet.Username); + } + + if (packet.Password != null) + { + packetWriter.WriteWithLengthPrefix(packet.Password); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + private static byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); + + if (!packet.ReasonCode.HasValue) + { + ThrowReasonCodeNotSetException(); + } + + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + packetWriter.Write(connectAcknowledgeFlags); + packetWriter.Write((byte)packet.ReasonCode.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); + propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); + propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); + propertiesWriter.WriteRetainAvailable(packet.Properties.RetainAvailable); + propertiesWriter.WriteReceiveMaximum(packet.Properties.ReceiveMaximum); + propertiesWriter.WriteAssignedClientIdentifier(packet.Properties.AssignedClientIdentifier); + propertiesWriter.WriteTopicAliasMaximum(packet.Properties.TopicAliasMaximum); + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteMaximumPacketSize(packet.Properties.MaximumPacketSize); + propertiesWriter.WriteWildcardSubscriptionAvailable(packet.Properties.WildcardSubscriptionAvailable); + propertiesWriter.WriteSubscriptionIdentifiersAvailable(packet.Properties.SubscriptionIdentifiersAvailable); + propertiesWriter.WriteSharedSubscriptionAvailable(packet.Properties.SharedSubscriptionAvailable); + propertiesWriter.WriteServerKeepAlive(packet.Properties.ServerKeepAlive); + propertiesWriter.WriteResponseInformation(packet.Properties.ResponseInformation); + propertiesWriter.WriteServerReference(packet.Properties.ServerReference); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + private static byte EncodePublishPacket(MqttPublishPacket packet, IMqttPacketWriter packetWriter) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); + + if (packet.QualityOfServiceLevel == 0 && packet.Dup) + { + throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); + } + + packetWriter.WriteWithLengthPrefix(packet.Topic); + + if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + } + + packetWriter.Write(packet.PacketIdentifier.Value); + } + else + { + if (packet.PacketIdentifier > 0) + { + throw new MqttProtocolViolationException("Packet identifier must be 0 if QoS == 0 [MQTT-2.3.1-5]."); + } + } + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WritePayloadFormatIndicator(packet.Properties.PayloadFormatIndicator); + propertiesWriter.WriteMessageExpiryInterval(packet.Properties.MessageExpiryInterval); + propertiesWriter.WriteTopicAlias(packet.Properties.TopicAlias); + propertiesWriter.WriteResponseTopic(packet.Properties.ResponseTopic); + propertiesWriter.WriteCorrelationData(packet.Properties.CorrelationData); + propertiesWriter.WriteSubscriptionIdentifier(packet.Properties.SubscriptionIdentifier); + propertiesWriter.WriteContentType(packet.Properties.ContentType); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + if (packet.Payload?.Length > 0) + { + packetWriter.Write(packet.Payload, 0, packet.Payload.Length); + } + + byte fixedHeader = 0; + + if (packet.Retain) + { + fixedHeader |= 0x01; + } + + fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); + + if (packet.Dup) + { + fixedHeader |= 0x08; + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); + } + + private static byte EncodePubAckPacket(MqttPubAckPacket packet, IMqttPacketWriter packetWriter) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); + + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); + } + + if (!packet.ReasonCode.HasValue) + { + throw new MqttProtocolViolationException("PubAck packet must contain a reason code."); + } + + packetWriter.Write(packet.PacketIdentifier.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubAckReasonCode.Success) + { + packetWriter.Write((byte)packet.ReasonCode.Value); + propertiesWriter.WriteToPacket(packetWriter); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); + } + + private static byte EncodePubRecPacket(MqttPubRecPacket packet, IMqttPacketWriter packetWriter) + { + ThrowIfPacketIdentifierIsInvalid(packet); + + if (!packet.ReasonCode.HasValue) + { + ThrowReasonCodeNotSetException(); + } + + packetWriter.Write(packet.PacketIdentifier.Value); + packetWriter.Write((byte)packet.ReasonCode.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubRecReasonCode.Success) + { + packetWriter.Write((byte)packet.ReasonCode.Value); + propertiesWriter.WriteToPacket(packetWriter); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); + } + + private static byte EncodePubRelPacket(MqttPubRelPacket packet, IMqttPacketWriter packetWriter) + { + ThrowIfPacketIdentifierIsInvalid(packet); + + if (!packet.ReasonCode.HasValue) + { + ThrowReasonCodeNotSetException(); + } + + packetWriter.Write(packet.PacketIdentifier.Value); + packetWriter.Write((byte)packet.ReasonCode.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubRelReasonCode.Success) + { + packetWriter.Write((byte)packet.ReasonCode.Value); + propertiesWriter.WriteToPacket(packetWriter); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); + } + + private static byte EncodePubCompPacket(MqttPubCompPacket packet, IMqttPacketWriter packetWriter) + { + ThrowIfPacketIdentifierIsInvalid(packet); + + if (!packet.ReasonCode.HasValue) + { + ThrowReasonCodeNotSetException(); + } + + packetWriter.Write(packet.PacketIdentifier.Value); + packetWriter.Write((byte)packet.ReasonCode.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubCompReasonCode.Success) + { + packetWriter.Write((byte)packet.ReasonCode.Value); + propertiesWriter.WriteToPacket(packetWriter); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); + } + + private static byte EncodeSubscribePacket(MqttSubscribePacket packet, IMqttPacketWriter packetWriter) + { + if (packet.TopicFilters?.Any() != true) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); + + ThrowIfPacketIdentifierIsInvalid(packet); + + packetWriter.Write(packet.PacketIdentifier.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteSubscriptionIdentifier(packet.Properties.SubscriptionIdentifier); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + if (packet.TopicFilters?.Count > 0) + { + foreach (var topicFilter in packet.TopicFilters) + { + packetWriter.WriteWithLengthPrefix(topicFilter.Topic); + + var options = (byte)topicFilter.QualityOfServiceLevel; + + if (topicFilter.NoLocal == true) + { + options |= 1 << 2; + } + + if (topicFilter.RetainAsPublished == true) + { + options |= 1 << 3; + } + + if (topicFilter.RetainHandling.HasValue) + { + options |= (byte)((byte)topicFilter.RetainHandling.Value << 4); + } + + packetWriter.Write(options); + } + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); + } + + private static byte EncodeSubAckPacket(MqttSubAckPacket packet, IMqttPacketWriter packetWriter) + { + if (packet.ReasonCodes?.Any() != true) throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); + + ThrowIfPacketIdentifierIsInvalid(packet); + + packetWriter.Write(packet.PacketIdentifier.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + foreach (var reasonCode in packet.ReasonCodes) + { + packetWriter.Write((byte)reasonCode); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.SubAck); + } + + private static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, IMqttPacketWriter packetWriter) + { + if (packet.TopicFilters?.Any() != true) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); + + ThrowIfPacketIdentifierIsInvalid(packet); + + packetWriter.Write(packet.PacketIdentifier.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + foreach (var topicFilter in packet.TopicFilters) + { + packetWriter.WriteWithLengthPrefix(topicFilter); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); + } + + private static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, IMqttPacketWriter packetWriter) + { + if (packet.ReasonCodes?.Any() != true) throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); + + ThrowIfPacketIdentifierIsInvalid(packet); + + packetWriter.Write(packet.PacketIdentifier.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + foreach (var reasonCode in packet.ReasonCodes) + { + packetWriter.Write((byte)reasonCode); + } + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); + } + + private static byte EncodeDisconnectPacket(MqttDisconnectPacket packet, IMqttPacketWriter packetWriter) + { + if (!packet.ReasonCode.HasValue) + { + ThrowReasonCodeNotSetException(); + } + + packetWriter.Write((byte)packet.ReasonCode.Value); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteServerReference(packet.Properties.ServerReference); + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Disconnect); + } + + private static byte EncodePingReqPacket() + { + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PingReq); + } + + private static byte EncodePingRespPacket() + { + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PingResp); + } + + private static byte EncodeAuthPacket(MqttAuthPacket packet, IMqttPacketWriter packetWriter) + { + packetWriter.Write((byte)packet.ReasonCode); + + var propertiesWriter = new MqttV500PropertiesWriter(); + if (packet.Properties != null) + { + propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); + propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); + propertiesWriter.WriteReasonString(packet.Properties.ReasonString); + propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); + } + + propertiesWriter.WriteToPacket(packetWriter); + + return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Auth); + } + + private static void ThrowReasonCodeNotSetException() + { + throw new MqttProtocolViolationException("The ReasonCode must be set for MQTT version 5."); + } + + private static void ThrowIfPacketIdentifierIsInvalid(IMqttPacketWithIdentifier packet) + { + if (!packet.PacketIdentifier.HasValue) + { + throw new MqttProtocolViolationException($"Packet identifier is not set for {packet.GetType().Name}."); + } + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs new file mode 100644 index 00000000..9a52a98d --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs @@ -0,0 +1,43 @@ +using System; +using MQTTnet.Adapter; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500PacketFormatter : IMqttPacketFormatter + { + private readonly MqttV500PacketEncoder _encoder; + private readonly MqttV500PacketDecoder _decoder = new MqttV500PacketDecoder(); + + public MqttV500PacketFormatter() + { + _encoder = new MqttV500PacketEncoder(); + } + + public MqttV500PacketFormatter(IMqttPacketWriter writer) + { + _encoder = new MqttV500PacketEncoder(writer); + } + + public IMqttDataConverter DataConverter { get; } = new MqttV500DataConverter(); + + public ArraySegment Encode(MqttBasePacket mqttPacket) + { + if (mqttPacket == null) throw new ArgumentNullException(nameof(mqttPacket)); + + return _encoder.Encode(mqttPacket); + } + + public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); + + return _decoder.Decode(receivedMqttPacket); + } + + public void FreeBuffer() + { + _encoder.FreeBuffer(); + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs b/Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs new file mode 100644 index 00000000..e30cb3f2 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500PropertiesReader + { + private readonly IMqttPacketBodyReader _body; + private readonly uint _length; + private readonly ulong _targetOffset; + + public MqttV500PropertiesReader(IMqttPacketBodyReader body) + { + _body = body ?? throw new ArgumentNullException(nameof(body)); + + if (!body.EndOfStream) + { + _length = body.ReadVariableLengthInteger(); + } + + _targetOffset = body.Offset + _length; + } + + public MqttPropertyId CurrentPropertyId { get; private set; } + + public bool MoveNext() + { + if (_length == 0) + { + return false; + } + + if (_body.Offset >= _targetOffset) + { + return false; + } + + CurrentPropertyId = (MqttPropertyId)_body.ReadByte(); + return true; + } + + public void FillUserProperties(List userProperties) + { + if (userProperties == null) throw new ArgumentNullException(nameof(userProperties)); + + var userPropertiesLength = _body.ReadVariableLengthInteger(); + if (userPropertiesLength == 0) + { + return; + } + + var targetPosition = _body.Offset + userPropertiesLength; + while (_body.Offset < targetPosition) + { + var name = _body.ReadStringWithLengthPrefix(); + var value = _body.ReadStringWithLengthPrefix(); + + userProperties.Add(new MqttUserProperty(name, value)); + } + } + + public string ReadReasonString() + { + return _body.ReadStringWithLengthPrefix(); + } + + public string ReadAuthenticationMethod() + { + return _body.ReadStringWithLengthPrefix(); + } + + public byte[] ReadAuthenticationData() + { + return _body.ReadWithLengthPrefix(); + } + + public bool? ReadRetainAvailable() + { + return _body.ReadBoolean(); + } + + public uint? ReadSessionExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public ushort? ReadReceiveMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadAssignedClientIdentifier() + { + return _body.ReadStringWithLengthPrefix(); + } + + public string ReadServerReference() + { + return _body.ReadStringWithLengthPrefix(); + } + + public ushort? ReadTopicAliasMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public uint? ReadMaximumPacketSize() + { + return _body.ReadFourByteInteger(); + } + + public ushort? ReadServerKeepAlive() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadResponseInformation() + { + return _body.ReadStringWithLengthPrefix(); + } + + public bool? ReadSharedSubscriptionAvailable() + { + return _body.ReadBoolean(); + } + + public bool? ReadSubscriptionIdentifiersAvailable() + { + return _body.ReadBoolean(); + } + + public bool? ReadWildcardSubscriptionAvailable() + { + return _body.ReadBoolean(); + } + + public uint? ReadSubscriptionIdentifier() + { + return _body.ReadVariableLengthInteger(); + } + + public MqttPayloadFormatIndicator? ReadPayloadFormatIndicator() + { + return (MqttPayloadFormatIndicator)_body.ReadByte(); + } + + public uint? ReadMessageExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public ushort? ReadTopicAlias() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadResponseTopic() + { + return _body.ReadStringWithLengthPrefix(); + } + + public byte[] ReadCorrelationData() + { + return _body.ReadWithLengthPrefix(); + } + + public string ReadContentType() + { + return _body.ReadStringWithLengthPrefix(); + } + + public uint? ReadWillDelayInterval() + { + return _body.ReadFourByteInteger(); + } + + public bool? RequestResponseInformation() + { + return _body.ReadBoolean(); + } + + public bool? RequestProblemInformation() + { + return _body.ReadBoolean(); + } + + public void ThrowInvalidPropertyIdException(Type type) + { + throw new MqttProtocolViolationException($"Property ID '{CurrentPropertyId}' is not supported for package type '{type.Name}'."); + } + } +} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs new file mode 100644 index 00000000..9e5fe39a --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public class MqttV500PropertiesWriter + { + private readonly MqttPacketWriter _packetWriter = new MqttPacketWriter(); + + public void WriteUserProperties(List userProperties) + { + if (userProperties == null || userProperties.Count == 0) + { + return; + } + + var propertyWriter = new MqttPacketWriter(); + foreach (var property in userProperties) + { + propertyWriter.WriteWithLengthPrefix(property.Name); + propertyWriter.WriteWithLengthPrefix(property.Value); + } + + _packetWriter.Write((byte)MqttPropertyId.UserProperty); + _packetWriter.WriteVariableLengthInteger((uint)propertyWriter.Length); + _packetWriter.Write(propertyWriter); + } + + public void WriteCorrelationData(byte[] value) + { + Write(MqttPropertyId.CorrelationData, value); + } + + public void WriteAuthenticationData(byte[] value) + { + Write(MqttPropertyId.AuthenticationData, value); + } + + public void WriteReasonString(string value) + { + Write(MqttPropertyId.ReasonString, value); + } + + public void WriteResponseTopic(string value) + { + Write(MqttPropertyId.ResponseTopic, value); + } + + public void WriteContentType(string value) + { + Write(MqttPropertyId.ContentType, value); + } + + public void WriteServerReference(string value) + { + Write(MqttPropertyId.ServerReference, value); + } + + public void WriteAuthenticationMethod(string value) + { + Write(MqttPropertyId.AuthenticationMethod, value); + } + + public void WriteToPacket(IMqttPacketWriter packetWriter) + { + if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); + + packetWriter.WriteVariableLengthInteger((uint)_packetWriter.Length); + packetWriter.Write(_packetWriter); + } + + public void WriteSessionExpiryInterval(uint? value) + { + WriteAsFourByteInteger(MqttPropertyId.SessionExpiryInterval, value); + } + + public void WriteSubscriptionIdentifier(uint? value) + { + WriteAsVariableLengthInteger(MqttPropertyId.SubscriptionIdentifier, value); + } + + public void WriteTopicAlias(ushort? value) + { + Write(MqttPropertyId.TopicAlias, value); + } + + public void WriteMessageExpiryInterval(uint? value) + { + WriteAsFourByteInteger(MqttPropertyId.MessageExpiryInterval, value); + } + + public void WritePayloadFormatIndicator(MqttPayloadFormatIndicator? value) + { + if (!value.HasValue) + { + return; + } + + Write(MqttPropertyId.PayloadFormatIndicator, (byte)value.Value); + } + + public void WriteWillDelayInterval(uint? value) + { + WriteAsFourByteInteger(MqttPropertyId.WillDelayInterval, value); + } + + public void WriteRequestProblemInformation(bool? value) + { + Write(MqttPropertyId.RequestProblemInformation, value); + } + + public void WriteRequestResponseInformation(bool? value) + { + Write(MqttPropertyId.RequestResponseInformation, value); + } + + public void WriteReceiveMaximum(ushort? value) + { + Write(MqttPropertyId.RequestResponseInformation, value); + } + + public void WriteMaximumPacketSize(uint? value) + { + WriteAsFourByteInteger(MqttPropertyId.MaximumPacketSize, value); + } + + public void WriteRetainAvailable(bool? value) + { + Write(MqttPropertyId.RetainAvailable, value); + } + + public void WriteAssignedClientIdentifier(string value) + { + Write(MqttPropertyId.AssignedClientIdentifer, value); + } + + public void WriteTopicAliasMaximum(ushort? value) + { + Write(MqttPropertyId.TopicAliasMaximum, value); + } + + public void WriteWildcardSubscriptionAvailable(bool? value) + { + Write(MqttPropertyId.WildcardSubscriptionAvailable, value); + } + + public void WriteSubscriptionIdentifiersAvailable(bool? value) + { + Write(MqttPropertyId.SubscriptionIdentifiersAvailable, value); + } + + public void WriteSharedSubscriptionAvailable(bool? value) + { + Write(MqttPropertyId.SharedSubscriptionAvailable, value); + } + + public void WriteServerKeepAlive(ushort? value) + { + Write(MqttPropertyId.ServerKeepAlive, value); + } + + public void WriteResponseInformation(string value) + { + Write(MqttPropertyId.ResponseInformation, value); + } + + private void Write(MqttPropertyId id, bool? value) + { + if (!value.HasValue) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.Write(value.Value ? (byte)0x1 : (byte)0x0); + } + + private void Write(MqttPropertyId id, ushort? value) + { + if (!value.HasValue) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.Write(value.Value); + } + + private void WriteAsVariableLengthInteger(MqttPropertyId id, uint? value) + { + if (!value.HasValue) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.WriteVariableLengthInteger(value.Value); + } + + private void WriteAsFourByteInteger(MqttPropertyId id, uint? value) + { + if (!value.HasValue) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.Write((byte)(value.Value >> 24)); + _packetWriter.Write((byte)(value.Value >> 16)); + _packetWriter.Write((byte)(value.Value >> 8)); + _packetWriter.Write((byte)value.Value); + } + + private void Write(MqttPropertyId id, string value) + { + if (value == null) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.WriteWithLengthPrefix(value); + } + + private void Write(MqttPropertyId id, byte[] value) + { + if (value == null) + { + return; + } + + _packetWriter.Write((byte)id); + _packetWriter.WriteWithLengthPrefix(value); + } + } +} diff --git a/Source/MQTTnet/IApplicationMessagePublisher.cs b/Source/MQTTnet/IApplicationMessagePublisher.cs index c47893af..bf17718c 100644 --- a/Source/MQTTnet/IApplicationMessagePublisher.cs +++ b/Source/MQTTnet/IApplicationMessagePublisher.cs @@ -1,9 +1,11 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client.Publishing; namespace MQTTnet { public interface IApplicationMessagePublisher { - Task PublishAsync(MqttApplicationMessage applicationMessage); + Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken); } } diff --git a/Source/MQTTnet/IApplicationMessageReceiver.cs b/Source/MQTTnet/IApplicationMessageReceiver.cs index b315067c..4e474856 100644 --- a/Source/MQTTnet/IApplicationMessageReceiver.cs +++ b/Source/MQTTnet/IApplicationMessageReceiver.cs @@ -1,9 +1,9 @@ -using System; +using MQTTnet.Client.Receiving; namespace MQTTnet { public interface IApplicationMessageReceiver { - event EventHandler ApplicationMessageReceived; + IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } } } diff --git a/Source/MQTTnet/IMqttFactory.cs b/Source/MQTTnet/IMqttFactory.cs new file mode 100644 index 00000000..487d77f4 --- /dev/null +++ b/Source/MQTTnet/IMqttFactory.cs @@ -0,0 +1,11 @@ +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Server; + +namespace MQTTnet +{ + public interface IMqttFactory : IMqttClientFactory, IMqttServerFactory + { + IMqttNetLogger DefaultLogger { get; } + } +} diff --git a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs index dd06c31a..2377cde7 100644 --- a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -1,8 +1,8 @@ using System; using MQTTnet.Adapter; -using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Diagnostics; -using MQTTnet.Serializer; +using MQTTnet.Formatter; namespace MQTTnet.Implementations { @@ -11,19 +11,17 @@ namespace MQTTnet.Implementations public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options, IMqttNetChildLogger logger) { if (options == null) throw new ArgumentNullException(nameof(options)); - - var serializer = new MqttPacketSerializer { ProtocolVersion = options.ProtocolVersion }; - + switch (options.ChannelOptions) { case MqttClientTcpOptions _: { - return new MqttChannelAdapter(new MqttTcpChannel(options), serializer, logger); + return new MqttChannelAdapter(new MqttTcpChannel(options), new MqttPacketFormatterAdapter(options.ProtocolVersion), logger); } case MqttClientWebSocketOptions webSocketOptions: { - return new MqttChannelAdapter(new MqttWebSocketChannel(webSocketOptions), serializer, logger); + return new MqttChannelAdapter(new MqttWebSocketChannel(webSocketOptions), new MqttPacketFormatterAdapter(options.ProtocolVersion), logger); } default: diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs index 6da9da24..1c0f944d 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs @@ -4,13 +4,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; +using System.Security.Authentication; 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.Client.Options; using MQTTnet.Server; namespace MQTTnet.Implementations @@ -36,33 +37,28 @@ namespace MQTTnet.Implementations _bufferSize = serverOptions.DefaultEndpointOptions.BufferSize; CreateStreams(); + + IsSecureConnection = socket.Information.ProtectionLevel >= SocketProtectionLevel.Tls12; + + Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; } public static Func> CustomIgnorableServerCertificateErrorsResolver { get; set; } - public string Endpoint - { - get - { - if (_socket?.Information != null) - { - return _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; - } + public string Endpoint { get; private set; } - return null; - } - } + public bool IsSecureConnection { get; } public async Task ConnectAsync(CancellationToken cancellationToken) { if (_socket == null) { _socket = new StreamSocket(); - _socket.Control.NoDelay = true; + _socket.Control.NoDelay = _options.NoDelay; _socket.Control.KeepAlive = true; } - if (!_options.TlsOptions.UseTls) + if (_options.TlsOptions?.UseTls != true) { await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString()); } @@ -75,13 +71,25 @@ namespace MQTTnet.Implementations _socket.Control.IgnorableServerCertificateErrors.Add(ignorableChainValidationResult); } - await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString(), SocketProtectionLevel.Tls12); + 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); } + Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; + CreateStreams(); } - public Task DisconnectAsync() + public Task DisconnectAsync(CancellationToken cancellationToken) { Dispose(); return Task.FromResult(0); diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index 58eece6a..cd4e2900 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -6,9 +6,10 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.IO; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using MQTTnet.Channel; -using MQTTnet.Client; +using MQTTnet.Client.Options; namespace MQTTnet.Implementations { @@ -17,100 +18,157 @@ namespace MQTTnet.Implementations private readonly IMqttClientOptions _clientOptions; private readonly MqttClientTcpOptions _options; - private Socket _socket; private Stream _stream; - /// - /// called on client sockets are created in connect - /// public MqttTcpChannel(IMqttClientOptions clientOptions) { _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); _options = (MqttClientTcpOptions)clientOptions.ChannelOptions; + + IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; } - /// - /// called on server, sockets are passed in - /// connect will not be called - /// - public MqttTcpChannel(Socket socket, SslStream sslStream) + public MqttTcpChannel(Stream stream, string endpoint) { - _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - CreateStream(sslStream); + IsSecureConnection = stream is SslStream; + Endpoint = endpoint; } - [Obsolete("There is a new callback at the TLS options. This one will be deleted soon.")] - public static Func CustomCertificateValidationCallback { get; set; } + public string Endpoint { get; private set; } - public string Endpoint => _socket?.RemoteEndPoint?.ToString(); + public bool IsSecureConnection { get; } public async Task ConnectAsync(CancellationToken cancellationToken) { - if (_socket == null) + Socket socket; + + if (_options.AddressFamily == AddressFamily.Unspecified) { - _socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + } + else + { + socket = new Socket(_options.AddressFamily, SocketType.Stream, ProtocolType.Tcp); } -#if NET452 || NET461 - await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, _options.Server, _options.GetPort(), null).ConfigureAwait(false); -#else - await _socket.ConnectAsync(_options.Server, _options.GetPort()).ConfigureAwait(false); -#endif + socket.ReceiveBufferSize = _options.BufferSize; + socket.SendBufferSize = _options.BufferSize; + socket.DualMode = _options.DualMode; + socket.NoDelay = _options.NoDelay; + + // Workaround for: workaround for https://github.com/dotnet/corefx/issues/24430 + using (cancellationToken.Register(() => socket.Dispose())) + { +#if NET452 || NET461 + await Task.Factory.FromAsync(socket.BeginConnect, socket.EndConnect, _options.Server, _options.GetPort(), null).ConfigureAwait(false); +#else + await socket.ConnectAsync(_options.Server, _options.GetPort()).ConfigureAwait(false); +#endif + } + + var networkStream = new NetworkStream(socket, true); - SslStream sslStream = null; if (_options.TlsOptions.UseTls) { - sslStream = new SslStream(new NetworkStream(_socket, true), false, InternalUserCertificateValidationCallback); + var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback); await sslStream.AuthenticateAsClientAsync(_options.Server, LoadCertificates(), _options.TlsOptions.SslProtocol, _options.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); + + _stream = sslStream; + } + else + { + _stream = networkStream; } - CreateStream(sslStream); + Endpoint = socket.RemoteEndPoint?.ToString(); } - public Task DisconnectAsync() + public Task DisconnectAsync(CancellationToken cancellationToken) { Dispose(); return Task.FromResult(0); } - public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return _stream.ReadAsync(buffer, offset, count, cancellationToken); + try + { + // Workaround for: https://github.com/dotnet/corefx/issues/24430 + using (cancellationToken.Register(Dispose)) + { + if (cancellationToken.IsCancellationRequested) + { + return 0; + } + + return await _stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) + { + ExceptionDispatchInfo.Capture(socketException).Throw(); + } + + throw; + } } - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return _stream.WriteAsync(buffer, offset, count, cancellationToken); + try + { + // Workaround for: https://github.com/dotnet/corefx/issues/24430 + using (cancellationToken.Register(Dispose)) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) + { + ExceptionDispatchInfo.Capture(socketException).Throw(); + } + + throw; + } } public void Dispose() { - Cleanup(ref _stream, s => s.Dispose()); - Cleanup(ref _socket, s => + // 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 (s.Connected) - { - s.Shutdown(SocketShutdown.Both); - } - s.Dispose(); - }); + _stream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + + _stream = null; } private bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - // Try the instance callback. if (_options.TlsOptions.CertificateValidationCallback != null) { return _options.TlsOptions.CertificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _clientOptions); } - // Try static callback. - if (CustomCertificateValidationCallback != null) - { - return CustomCertificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _options); - } - if (sslPolicyErrors == SslPolicyErrors.None) { return true; @@ -150,37 +208,6 @@ namespace MQTTnet.Implementations return certificates; } - - private void CreateStream(Stream stream) - { - if (stream != null) - { - _stream = stream; - } - else - { - _stream = new NetworkStream(_socket, true); - } - } - - private static void Cleanup(ref T item, Action handler) where T : class - { - var temp = item; - item = null; - try - { - if (temp != null) - { - handler(temp); - } - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - } } } #endif diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs index c42db2b6..45290757 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Windows.Networking.Sockets; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Serializer; +using MQTTnet.Formatter; using MQTTnet.Server; namespace MQTTnet.Implementations @@ -23,7 +23,7 @@ namespace MQTTnet.Implementations _logger = logger.CreateChildLogger(nameof(MqttTcpServerAdapter)); } - public event EventHandler ClientAccepted; + public Func ClientHandler { get; set; } public async Task StartAsync(IMqttServerOptions options) { @@ -36,10 +36,10 @@ namespace MQTTnet.Implementations _listener = new StreamSocketListener(); // This also affects the client sockets. - _listener.Control.NoDelay = true; + _listener.Control.NoDelay = options.DefaultEndpointOptions.NoDelay; _listener.Control.KeepAlive = true; _listener.Control.QualityOfService = SocketQualityOfService.LowLatency; - _listener.ConnectionReceived += AcceptDefaultEndpointConnectionsAsync; + _listener.ConnectionReceived += OnConnectionReceivedAsync; await _listener.BindServiceNameAsync(options.DefaultEndpointOptions.Port.ToString(), SocketProtectionLevel.PlainSocket); } @@ -54,30 +54,51 @@ namespace MQTTnet.Implementations { if (_listener != null) { - _listener.ConnectionReceived -= AcceptDefaultEndpointConnectionsAsync; + _listener.ConnectionReceived -= OnConnectionReceivedAsync; } - _listener?.Dispose(); - _listener = null; - return Task.FromResult(0); } public void Dispose() { - StopAsync().GetAwaiter().GetResult(); + _listener?.Dispose(); + _listener = null; } - private void AcceptDefaultEndpointConnectionsAsync(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) + private async void OnConnectionReceivedAsync(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) { try { - var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket, _options), new MqttPacketSerializer(), _logger); - ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); + var clientHandler = ClientHandler; + if (clientHandler != null) + { + using (var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket, _options), new MqttPacketFormatterAdapter(), _logger)) + { + await clientHandler(clientAdapter).ConfigureAwait(false); + } + } } catch (Exception exception) { - _logger.Error(exception, "Error while accepting connection at default endpoint."); + 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"); + } } } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs index cfbf7251..900ba965 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -26,7 +26,7 @@ namespace MQTTnet.Implementations _logger = logger.CreateChildLogger(nameof(MqttTcpServerAdapter)); } - public event EventHandler ClientAccepted; + public Func ClientHandler { get; set; } public Task StartAsync(IMqttServerOptions options) { @@ -87,9 +87,11 @@ namespace MQTTnet.Implementations options, tlsCertificate, _cancellationTokenSource.Token, - _logger); + _logger) + { + ClientHandler = OnClientAcceptedAsync + }; - listenerV4.ClientAccepted += OnClientAccepted; listenerV4.Start(); _listeners.Add(listenerV4); } @@ -101,17 +103,25 @@ namespace MQTTnet.Implementations options, tlsCertificate, _cancellationTokenSource.Token, - _logger); + _logger) + { + ClientHandler = OnClientAcceptedAsync + }; - listenerV6.ClientAccepted += OnClientAccepted; listenerV6.Start(); _listeners.Add(listenerV6); } } - private void OnClientAccepted(object sender, MqttServerAdapterClientAcceptedEventArgs e) + private Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter) { - ClientAccepted?.Invoke(this, e); + var clientHandler = ClientHandler; + if (clientHandler == null) + { + return Task.FromResult(0); + } + + return clientHandler(channelAdapter); } } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs index e72c831d..cc8246ac 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -1,15 +1,15 @@ #if !WINDOWS_UWP using System; +using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; -using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Serializer; +using MQTTnet.Formatter; using MQTTnet.Server; namespace MQTTnet.Implementations @@ -36,14 +36,14 @@ namespace MQTTnet.Implementations _tlsCertificate = tlsCertificate; _cancellationToken = cancellationToken; _logger = logger.CreateChildLogger(nameof(MqttTcpServerListener)); - + if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) { _tlsOptions = tlsOptions; } } - public event EventHandler ClientAccepted; + public Func ClientHandler { get; set; } public void Start() { @@ -59,53 +59,7 @@ namespace MQTTnet.Implementations _logger.Info($"Starting TCP listener for {_socket.LocalEndPoint} TLS={_tlsCertificate != null}."); _socket.Listen(_options.ConnectionBacklog); - Task.Run(AcceptClientConnectionsAsync, _cancellationToken); - } - - private async Task AcceptClientConnectionsAsync() - { - while (!_cancellationToken.IsCancellationRequested) - { - 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 - clientSocket.NoDelay = true; - - SslStream sslStream = null; - - if (_tlsCertificate != null) - { - sslStream = new SslStream(new NetworkStream(clientSocket), false); - await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, _tlsOptions.SslProtocol, false).ConfigureAwait(false); - } - - _logger.Verbose("Client '{0}' accepted by TCP listener '{1}, {2}'.", - clientSocket.RemoteEndPoint, - _socket.LocalEndPoint, - _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"); - - var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(clientSocket, sslStream), new MqttPacketSerializer(), _logger); - ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(clientAdapter)); - } - catch (ObjectDisposedException) - { - // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. - } - catch (Exception exception) - { - if (exception is SocketException s && s.SocketErrorCode == SocketError.OperationAborted) - { - return; - } - - _logger.Error(exception, $"Error while accepting connection at TCP listener {_socket.LocalEndPoint} TLS={_tlsCertificate != null}."); - await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken).ConfigureAwait(false); - } - } + Task.Run(() => AcceptClientConnectionsAsync(_cancellationToken), _cancellationToken); } public void Dispose() @@ -116,6 +70,98 @@ namespace MQTTnet.Implementations _tlsCertificate?.Dispose(); #endif } + + private async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + 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 +#pragma warning disable 4014 + Task.Run(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken); +#pragma warning restore 4014 + } + catch (Exception exception) + { + _logger.Error(exception, $"Error while accepting connection at TCP listener {_socket.LocalEndPoint} TLS={_tlsCertificate != null}."); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + } + } + } + + private async Task TryHandleClientConnectionAsync(Socket clientSocket) + { + Stream stream = null; + string remoteEndPoint = null; + + try + { + remoteEndPoint = clientSocket.RemoteEndPoint.ToString(); + + _logger.Verbose("Client '{0}' accepted by TCP listener '{1}, {2}'.", + remoteEndPoint, + _socket.LocalEndPoint, + _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"); + + clientSocket.NoDelay = _options.NoDelay; + + stream = new NetworkStream(clientSocket, true); + + if (_tlsCertificate != null) + { + var sslStream = new SslStream(stream, false); + await sslStream.AuthenticateAsServerAsync(_tlsCertificate, false, _tlsOptions.SslProtocol, false).ConfigureAwait(false); + stream = sslStream; + } + + var clientHandler = ClientHandler; + if (clientHandler != null) + { + using (var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(stream, remoteEndPoint), new MqttPacketFormatterAdapter(), _logger)) + { + 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(); + + _logger.Verbose("Client '{0}' disconnected at TCP listener '{1}, {2}'.", + remoteEndPoint, + _socket.LocalEndPoint, + _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"); + } + catch (Exception disposeException) + { + _logger.Error(disposeException, "Error while cleaning up client connection"); + } + } + } } } #endif \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index 53173d27..ccc601a7 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Channel; -using MQTTnet.Client; +using MQTTnet.Client.Options; namespace MQTTnet.Implementations { @@ -21,14 +21,18 @@ namespace MQTTnet.Implementations _options = options ?? throw new ArgumentNullException(nameof(options)); } - public MqttWebSocketChannel(WebSocket webSocket, string endpoint) + public MqttWebSocketChannel(WebSocket webSocket, string endpoint, bool isSecureConnection) { _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); + Endpoint = endpoint; + IsSecureConnection = isSecureConnection; } public string Endpoint { get; } + public bool IsSecureConnection { get; private set; } + public async Task ConnectAsync(CancellationToken cancellationToken) { var uri = _options.Uri; @@ -83,9 +87,11 @@ namespace MQTTnet.Implementations await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); _webSocket = clientWebSocket; + + IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(CancellationToken cancellationToken) { if (_webSocket == null) { @@ -94,7 +100,7 @@ namespace MQTTnet.Implementations if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); } Dispose(); @@ -155,9 +161,7 @@ namespace MQTTnet.Implementations if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) { - var credentials = - new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); - + var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); return new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList, credentials); } diff --git a/Source/MQTTnet/Serializer/Extensions.cs b/Source/MQTTnet/Internal/ArraySegmentExtensions.cs similarity index 85% rename from Source/MQTTnet/Serializer/Extensions.cs rename to Source/MQTTnet/Internal/ArraySegmentExtensions.cs index 00ad7cfb..f0054f47 100644 --- a/Source/MQTTnet/Serializer/Extensions.cs +++ b/Source/MQTTnet/Internal/ArraySegmentExtensions.cs @@ -1,8 +1,8 @@ using System; -namespace MQTTnet.Serializer +namespace MQTTnet.Internal { - public static class Extensions + public static class ArraySegmentExtensions { public static byte[] ToArray(this ArraySegment source) { diff --git a/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs b/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs index a6e96211..cd62f074 100644 --- a/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs +++ b/Source/MQTTnet/Internal/AsyncAutoResetEvent.cs @@ -74,8 +74,11 @@ namespace MQTTnet.Internal Task winner; if (timeout == Timeout.InfiniteTimeSpan) { - await tcs.Task.ConfigureAwait(false); - winner = tcs.Task; + using (cancellationToken.Register(() => { tcs.TrySetCanceled(); })) + { + await tcs.Task.ConfigureAwait(false); + winner = tcs.Task; + } } else { @@ -122,7 +125,7 @@ namespace MQTTnet.Internal } } - toRelease?.SetResult(true); + toRelease?.TrySetResult(true); } } } diff --git a/Source/MQTTnet/Internal/AsyncBlockingQueue.cs b/Source/MQTTnet/Internal/AsyncBlockingQueue.cs new file mode 100644 index 00000000..6cb80d2d --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncBlockingQueue.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + public sealed class AsyncQueue : IDisposable + { + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0); + + private ConcurrentQueue _queue = new ConcurrentQueue(); + + public int Count => _queue.Count; + + public void Enqueue(TItem item) + { + _queue.Enqueue(item); + _semaphore.Release(); + } + + public async Task> TryDequeueAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + } + + return new AsyncQueueDequeueResult(false, default(TItem)); + } + + public AsyncQueueDequeueResult TryDequeue() + { + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + + return new AsyncQueueDequeueResult(false, default(TItem)); + } + + public void Clear() + { + Interlocked.Exchange(ref _queue, new ConcurrentQueue()); + } + + public void Dispose() + { + _semaphore?.Dispose(); + } + } +} diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs index 87571c2a..9b7eefdd 100644 --- a/Source/MQTTnet/Internal/AsyncLock.cs +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -15,14 +15,23 @@ namespace MQTTnet.Internal _releaser = Task.FromResult((IDisposable)new Releaser(this)); } - public Task LockAsync(CancellationToken cancellationToken) + public Task WaitAsync() { - var wait = _semaphore.WaitAsync(cancellationToken); - return wait.IsCompleted ? - _releaser : - wait.ContinueWith((_, state) => (IDisposable)state, - _releaser.Result, cancellationToken, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + return WaitAsync(CancellationToken.None); + } + + public Task WaitAsync(CancellationToken cancellationToken) + { + var task = _semaphore.WaitAsync(cancellationToken); + if (task.IsCompleted) + { + return _releaser; + } + + return task.ContinueWith( + (_, state) => (IDisposable)state, + _releaser.Result, + cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } public void Dispose() diff --git a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs new file mode 100644 index 00000000..3e2b07b8 --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs @@ -0,0 +1,15 @@ +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/MQTTnet/Internal/ExceptionHelper.cs b/Source/MQTTnet/Internal/ExceptionHelper.cs deleted file mode 100644 index 5bc8e435..00000000 --- a/Source/MQTTnet/Internal/ExceptionHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MQTTnet.Exceptions; - -namespace MQTTnet.Internal -{ - public static class ExceptionHelper - { - public static void ThrowGracefulSocketClose() - { - throw new MqttCommunicationClosedGracefullyException(); - } - - public static void ThrowIfGracefulSocketClose(int readBytesCount) - { - if (readBytesCount <= 0) - { - throw new MqttCommunicationClosedGracefullyException(); - } - } - } -} diff --git a/Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs b/Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs deleted file mode 100644 index 79eb2c43..00000000 --- a/Source/MQTTnet/Internal/MqttApplicationMessageExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MQTTnet.Packets; - -namespace MQTTnet.Internal -{ - public static class MqttApplicationMessageExtensions - { - public static MqttApplicationMessage ToApplicationMessage(this MqttPublishPacket publishPacket) - { - return new MqttApplicationMessage - { - Topic = publishPacket.Topic, - Payload = publishPacket.Payload, - QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, - Retain = publishPacket.Retain - }; - } - - public static MqttPublishPacket ToPublishPacket(this MqttApplicationMessage applicationMessage) - { - if (applicationMessage == null) - { - return null; - } - - return new MqttPublishPacket - { - Topic = applicationMessage.Topic, - Payload = applicationMessage.Payload, - QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, - Retain = applicationMessage.Retain, - Dup = false - }; - } - } -} diff --git a/Source/MQTTnet/Internal/TaskExtensions.cs b/Source/MQTTnet/Internal/MqttTaskTimeout.cs similarity index 84% rename from Source/MQTTnet/Internal/TaskExtensions.cs rename to Source/MQTTnet/Internal/MqttTaskTimeout.cs index 1356d97d..ba4ec7fd 100644 --- a/Source/MQTTnet/Internal/TaskExtensions.cs +++ b/Source/MQTTnet/Internal/MqttTaskTimeout.cs @@ -5,9 +5,9 @@ using MQTTnet.Exceptions; namespace MQTTnet.Internal { - public static class TaskExtensions + public static class MqttTaskTimeout { - public static async Task TimeoutAfterAsync(Func action, TimeSpan timeout, CancellationToken cancellationToken) + public static async Task WaitAsync(Func action, TimeSpan timeout, CancellationToken cancellationToken) { if (action == null) throw new ArgumentNullException(nameof(action)); @@ -31,7 +31,7 @@ namespace MQTTnet.Internal } } - public static async Task TimeoutAfterAsync(Func> action, TimeSpan timeout, CancellationToken cancellationToken) + public static async Task WaitAsync(Func> action, TimeSpan timeout, CancellationToken cancellationToken) { if (action == null) throw new ArgumentNullException(nameof(action)); diff --git a/Source/MQTTnet/Internal/TestMqttChannel.cs b/Source/MQTTnet/Internal/TestMqttChannel.cs index 08920e3c..1ef8e5a5 100644 --- a/Source/MQTTnet/Internal/TestMqttChannel.cs +++ b/Source/MQTTnet/Internal/TestMqttChannel.cs @@ -16,12 +16,14 @@ namespace MQTTnet.Internal public string Endpoint { get; } = ""; + public bool IsSecureConnection { get; } = false; + public Task ConnectAsync(CancellationToken cancellationToken) { return Task.FromResult(0); } - public Task DisconnectAsync() + public Task DisconnectAsync(CancellationToken cancellationToken) { return Task.FromResult(0); } diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 3d3d80da..5ded88e4 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -54,7 +54,7 @@ - + @@ -62,5 +62,5 @@ - + \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessage.cs b/Source/MQTTnet/MqttApplicationMessage.cs index 334dfb40..06bd97a7 100644 --- a/Source/MQTTnet/MqttApplicationMessage.cs +++ b/Source/MQTTnet/MqttApplicationMessage.cs @@ -1,4 +1,6 @@ -using MQTTnet.Protocol; +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet { @@ -11,5 +13,21 @@ namespace MQTTnet public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } public bool Retain { get; set; } + + public List UserProperties { get; set; } + + public string ContentType { get; set; } + + public string ResponseTopic { get; set; } + + public MqttPayloadFormatIndicator? PayloadFormatIndicator { get; set; } + + public uint? MessageExpiryInterval { get; set; } + + public ushort? TopicAlias { get; set; } + + public byte[] CorrelationData { get; set; } + + public uint? SubscriptionIdentifier { get; set; } } } diff --git a/Source/MQTTnet/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs index 79c65ece..cc962b9f 100644 --- a/Source/MQTTnet/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; using MQTTnet.Exceptions; +using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet @@ -13,6 +14,14 @@ namespace MQTTnet private string _topic; private byte[] _payload; private bool _retain; + private string _contentType; + private string _responseTopic; + private byte[] _correlationData; + private ushort? _topicAlias; + private uint? _subscriptionIdentifier; + private uint? _messageExpiryInterval; + private MqttPayloadFormatIndicator? _payloadFormatIndicator; + private List _userProperties; public MqttApplicationMessageBuilder WithTopic(string topic) { @@ -28,12 +37,24 @@ namespace MQTTnet return this; } - _payload = payload.ToArray(); + _payload = payload as byte[]; + + if (_payload == null) + { + _payload = payload.ToArray(); + } + return this; } public MqttApplicationMessageBuilder WithPayload(Stream payload) { + if (payload == null) + { + _payload = null; + return this; + } + return WithPayload(payload, payload.Length - payload.Position); } @@ -47,7 +68,7 @@ namespace MQTTnet if (payload.Length == 0) { - _payload = new byte[0]; + _payload = null; } else { @@ -66,7 +87,7 @@ namespace MQTTnet return this; } - _payload = string.IsNullOrEmpty(payload) ? new byte[0] : Encoding.UTF8.GetBytes(payload); + _payload = string.IsNullOrEmpty(payload) ? null : Encoding.UTF8.GetBytes(payload); return this; } @@ -100,6 +121,83 @@ namespace MQTTnet return this; } + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, string value) + { + if (_userProperties == null) + { + _userProperties = new List(); + } + + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithContentType(string contentType) + { + _contentType = contentType; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithResponseTopic(string responseTopic) + { + _responseTopic = responseTopic; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithCorrelationData(byte[] correlationData) + { + _correlationData = correlationData; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) + { + _topicAlias = topicAlias; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) + { + _subscriptionIdentifier = subscriptionIdentifier; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) + { + _messageExpiryInterval = messageExpiryInterval; + return this; + } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) + { + _payloadFormatIndicator = payloadFormatIndicator; + return this; + } + public MqttApplicationMessage Build() { if (string.IsNullOrEmpty(_topic)) @@ -107,13 +205,27 @@ namespace MQTTnet throw new MqttProtocolViolationException("Topic is not set."); } - return new MqttApplicationMessage + var applicationMessage = new MqttApplicationMessage { Topic = _topic, - Payload = _payload ?? new byte[0], + Payload = _payload, QualityOfServiceLevel = _qualityOfServiceLevel, - Retain = _retain + Retain = _retain, + ContentType = _contentType, + ResponseTopic = _responseTopic, + CorrelationData = _correlationData, + TopicAlias = _topicAlias, + SubscriptionIdentifier = _subscriptionIdentifier, + MessageExpiryInterval = _messageExpiryInterval, + PayloadFormatIndicator = _payloadFormatIndicator }; + + if (_userProperties?.Any() == true) + { + applicationMessage.UserProperties = _userProperties; + } + + return applicationMessage; } } } diff --git a/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs b/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs index e81c69a9..6fbaa933 100644 --- a/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs +++ b/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs @@ -13,5 +13,7 @@ namespace MQTTnet public string ClientId { get; } public MqttApplicationMessage ApplicationMessage { get; } + + public bool ProcessingFailed { get; set; } } } diff --git a/Source/MQTTnet/MqttFactory.cs b/Source/MQTTnet/MqttFactory.cs index 3f1e1acb..ab6cc760 100644 --- a/Source/MQTTnet/MqttFactory.cs +++ b/Source/MQTTnet/MqttFactory.cs @@ -8,11 +8,22 @@ using MQTTnet.Server; namespace MQTTnet { - public class MqttFactory : IMqttClientFactory, IMqttServerFactory + public class MqttFactory : IMqttFactory { + public MqttFactory() : this(new MqttNetLogger()) + { + } + + public MqttFactory(IMqttNetLogger logger) + { + DefaultLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public IMqttNetLogger DefaultLogger { get; } + public IMqttClient CreateMqttClient() { - return CreateMqttClient(new MqttNetLogger()); + return CreateMqttClient(DefaultLogger); } public IMqttClient CreateMqttClient(IMqttNetLogger logger) @@ -26,7 +37,7 @@ namespace MQTTnet { if (adapterFactory == null) throw new ArgumentNullException(nameof(adapterFactory)); - return new MqttClient(adapterFactory, new MqttNetLogger()); + return new MqttClient(adapterFactory, DefaultLogger); } public IMqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory adapterFactory) @@ -39,8 +50,7 @@ namespace MQTTnet public IMqttServer CreateMqttServer() { - var logger = new MqttNetLogger(); - return CreateMqttServer(logger); + return CreateMqttServer(DefaultLogger); } public IMqttServer CreateMqttServer(IMqttNetLogger logger) @@ -57,5 +67,12 @@ namespace MQTTnet return new MqttServer(adapters, logger.CreateChildLogger()); } + + public IMqttServer CreateMqttServer(IEnumerable adapters) + { + if (adapters == null) throw new ArgumentNullException(nameof(adapters)); + + return new MqttServer(adapters, DefaultLogger.CreateChildLogger()); + } } } \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaiter.cs b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaiter.cs new file mode 100644 index 00000000..786dc97d --- /dev/null +++ b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaiter.cs @@ -0,0 +1,14 @@ +using System; +using MQTTnet.Packets; + +namespace MQTTnet.PacketDispatcher +{ + public interface IMqttPacketAwaiter : IDisposable + { + void Complete(MqttBasePacket packet); + + void Fail(Exception exception); + + void Cancel(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaiter.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaiter.cs new file mode 100644 index 00000000..19df6d45 --- /dev/null +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaiter.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Exceptions; +using MQTTnet.Packets; + +namespace MQTTnet.PacketDispatcher +{ + public sealed class MqttPacketAwaiter : IMqttPacketAwaiter where TPacket : MqttBasePacket + { + private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private readonly ushort? _packetIdentifier; + private readonly MqttPacketDispatcher _owningPacketDispatcher; + + public MqttPacketAwaiter(ushort? packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) + { + _packetIdentifier = packetIdentifier; + _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); + } + + public async Task WaitOneAsync(TimeSpan timeout) + { + using (var timeoutToken = new CancellationTokenSource(timeout)) + { + timeoutToken.Token.Register(() => _taskCompletionSource.TrySetException(new MqttCommunicationTimedOutException())); + + var packet = await _taskCompletionSource.Task.ConfigureAwait(false); + return (TPacket)packet; + } + } + + public void Complete(MqttBasePacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + // 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. + Task.Run(() => _taskCompletionSource.TrySetResult(packet)); + } + + public void Fail(Exception exception) + { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + + Task.Run(() => _taskCompletionSource.TrySetException(exception)); + } + + public void Cancel() + { + Task.Run(() => _taskCompletionSource.TrySetCanceled()); + } + + public void Dispose() + { + _owningPacketDispatcher.RemovePacketAwaiter(_packetIdentifier); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttPacketDispatcher.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs similarity index 52% rename from Source/MQTTnet/Client/MqttPacketDispatcher.cs rename to Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs index fd205c51..26b36ec1 100644 --- a/Source/MQTTnet/Client/MqttPacketDispatcher.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -1,22 +1,22 @@ using System; using System.Collections.Concurrent; -using System.Threading.Tasks; +using MQTTnet.Exceptions; using MQTTnet.Packets; -namespace MQTTnet.Client +namespace MQTTnet.PacketDispatcher { public class MqttPacketDispatcher { - private readonly ConcurrentDictionary, TaskCompletionSource> _awaiters = new ConcurrentDictionary, TaskCompletionSource>(); - + private readonly ConcurrentDictionary, IMqttPacketAwaiter> _packetAwaiters = new ConcurrentDictionary, IMqttPacketAwaiter>(); + public void Dispatch(Exception exception) { - foreach (var awaiter in _awaiters) + foreach (var awaiter in _packetAwaiters) { - Task.Run(() => awaiter.Value.TrySetException(exception)); // Task.Run fixes a dead lock. Without this the client only receives one message. + awaiter.Value.Fail(exception); } - _awaiters.Clear(); + _packetAwaiters.Clear(); } public void Dispatch(MqttBasePacket packet) @@ -31,37 +31,42 @@ namespace MQTTnet.Client var type = packet.GetType(); var key = new Tuple(identifier, type); - - if (_awaiters.TryRemove(key, out var awaiter)) + + if (_packetAwaiters.TryRemove(key, out var awaiter)) { - Task.Run(() => awaiter.TrySetResult(packet)); // Task.Run fixes a dead lock. Without this the client only receives one message. + awaiter.Complete(packet); return; } - throw new InvalidOperationException($"Packet of type '{type.Name}' not handled or dispatched."); + throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); } public void Reset() { - _awaiters.Clear(); + foreach (var awaiter in _packetAwaiters) + { + awaiter.Value.Cancel(); + } + + _packetAwaiters.Clear(); } - public TaskCompletionSource AddPacketAwaiter(ushort? identifier) where TResponsePacket : MqttBasePacket + public MqttPacketAwaiter AddPacketAwaiter(ushort? identifier) where TResponsePacket : MqttBasePacket { - var tcs = new TaskCompletionSource(); - if (!identifier.HasValue) { identifier = 0; } - - var key = new Tuple(identifier ?? 0, typeof(TResponsePacket)); - if (!_awaiters.TryAdd(key, tcs)) + + var awaiter = new MqttPacketAwaiter(identifier, this); + + var key = new Tuple(identifier.Value, typeof(TResponsePacket)); + if (!_packetAwaiters.TryAdd(key, awaiter)) { throw new InvalidOperationException($"The packet dispatcher already has an awaiter for packet of type '{key.Item2.Name}' with identifier {key.Item1}."); } - return tcs; + return awaiter; } public void RemovePacketAwaiter(ushort? identifier) where TResponsePacket : MqttBasePacket @@ -71,8 +76,8 @@ namespace MQTTnet.Client identifier = 0; } - var key = new Tuple(identifier ?? 0, typeof(TResponsePacket)); - _awaiters.TryRemove(key, out _); + var key = new Tuple(identifier.Value, typeof(TResponsePacket)); + _packetAwaiters.TryRemove(key, out _); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttAuthPacket.cs b/Source/MQTTnet/Packets/MqttAuthPacket.cs new file mode 100644 index 00000000..cf93a00a --- /dev/null +++ b/Source/MQTTnet/Packets/MqttAuthPacket.cs @@ -0,0 +1,14 @@ +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + /// + /// Added in MQTTv5.0.0. + /// + public class MqttAuthPacket : MqttBasePacket + { + public MqttAuthenticateReasonCode ReasonCode { get; set; } + + public MqttAuthPacketProperties Properties { get; set; } + } +} diff --git a/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs b/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs new file mode 100644 index 00000000..6ed25ee0 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttAuthPacketProperties + { + public string AuthenticationMethod { get; set; } + + public byte[] AuthenticationData { get; set; } + + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttConnAckPacket.cs b/Source/MQTTnet/Packets/MqttConnAckPacket.cs index dbdeea89..16a492c3 100644 --- a/Source/MQTTnet/Packets/MqttConnAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnAckPacket.cs @@ -4,13 +4,25 @@ namespace MQTTnet.Packets { public class MqttConnAckPacket : MqttBasePacket { + public MqttConnectReturnCode? ReturnCode { get; set; } + + #region Added in MQTTv3.1.1 + public bool IsSessionPresent { get; set; } - public MqttConnectReturnCode ConnectReturnCode { get; set; } + #endregion + + #region Added in MQTTv5.0.0 + + public MqttConnectReasonCode? ReasonCode { get; set; } + + public MqttConnAckPacketProperties Properties { get; set; } + + #endregion public override string ToString() { - return "ConnAck: [ConnectReturnCode=" + ConnectReturnCode + "] [IsSessionPresent=" + IsSessionPresent + "]"; + return string.Concat("ConnAck: [ReturnCode=", ReturnCode, "] [ReasonCode=", ReasonCode, "] [IsSessionPresent=", IsSessionPresent, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs new file mode 100644 index 00000000..262222db --- /dev/null +++ b/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttConnAckPacketProperties + { + public uint? SessionExpiryInterval { get; set; } + + public ushort? ReceiveMaximum { get; set; } + + public bool? RetainAvailable { get; set; } + + public uint? MaximumPacketSize { get; set; } + + public string AssignedClientIdentifier { get; set; } + + public ushort? TopicAliasMaximum { get; set; } + + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + + public bool? WildcardSubscriptionAvailable { get; set; } + + public bool? SubscriptionIdentifiersAvailable { get; set; } + + public bool? SharedSubscriptionAvailable { get; set; } + + public ushort? ServerKeepAlive { get; set; } + + public string ResponseInformation { get; set; } + + public string ServerReference { get; set; } + + public string AuthenticationMethod { get; set; } + + public byte[] AuthenticationData { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttConnectPacket.cs b/Source/MQTTnet/Packets/MqttConnectPacket.cs index 2134f4bb..3465280e 100644 --- a/Source/MQTTnet/Packets/MqttConnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnectPacket.cs @@ -1,11 +1,7 @@ -using MQTTnet.Serializer; - -namespace MQTTnet.Packets +namespace MQTTnet.Packets { public class MqttConnectPacket : MqttBasePacket { - public MqttProtocolVersion ProtocolVersion { get; set; } - public string ClientId { get; set; } public string Username { get; set; } @@ -14,13 +10,26 @@ namespace MQTTnet.Packets public ushort KeepAlivePeriod { get; set; } + // Also called "Clean Start" in MQTTv5. public bool CleanSession { get; set; } public MqttApplicationMessage WillMessage { get; set; } + #region Added in MQTTv5.0.0 + + public MqttConnectPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "Connect: [ClientId=" + ClientId + "] [Username=" + Username + "] [Password=" + Password + "] [KeepAlivePeriod=" + KeepAlivePeriod + "] [CleanSession=" + CleanSession + "]"; + var password = Password; + if (!string.IsNullOrEmpty(password)) + { + password = "****"; + } + + return string.Concat("Connect: [ClientId=", ClientId, "] [Username=", Username, "] [Password=", password, "] [KeepAlivePeriod=", KeepAlivePeriod, "] [CleanSession=", CleanSession, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs b/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs new file mode 100644 index 00000000..25cf5bda --- /dev/null +++ b/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttConnectPacketProperties + { + public uint? WillDelayInterval { get; set; } + + public uint? SessionExpiryInterval { get; set; } + + public string AuthenticationMethod { get; set; } + + public byte[] AuthenticationData { get; set; } + + public bool? RequestProblemInformation { get; set; } + + public bool? RequestResponseInformation { get; set; } + + public ushort? ReceiveMaximum { get; set; } + + public ushort? TopicAliasMaximum { get; set; } + + public uint? MaximumPacketSize { get; set; } + + public List UserProperties { get; } = new List(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs index 621829a2..c198d9a9 100644 --- a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs @@ -1,10 +1,20 @@ -namespace MQTTnet.Packets +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttDisconnectPacket : MqttBasePacket { + #region Added in MQTTv5 + + public MqttDisconnectReasonCode? ReasonCode { get; set; } + + public MqttDisconnectPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "Disconnect"; + return string.Concat("Disconnect: [ReasonCode=", ReasonCode, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs b/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs new file mode 100644 index 00000000..a5385cb0 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttDisconnectPacketProperties + { + public uint? SessionExpiryInterval { get; set; } + + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + + public string ServerReference { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubAckPacket.cs b/Source/MQTTnet/Packets/MqttPubAckPacket.cs index 5811849b..c9190259 100644 --- a/Source/MQTTnet/Packets/MqttPubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubAckPacket.cs @@ -1,10 +1,20 @@ -namespace MQTTnet.Packets +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttPubAckPacket : MqttBasePublishPacket { + #region Added in MQTTv5 + + public MqttPubAckReasonCode? ReasonCode { get; set; } + + public MqttPubAckPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return $"PubAck [PacketIdentifier={PacketIdentifier}]"; + return string.Concat("PubAck: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs new file mode 100644 index 00000000..061f72f9 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttPubAckPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttPubCompPacket.cs b/Source/MQTTnet/Packets/MqttPubCompPacket.cs index e9ea18eb..0798819d 100644 --- a/Source/MQTTnet/Packets/MqttPubCompPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubCompPacket.cs @@ -1,10 +1,20 @@ -namespace MQTTnet.Packets +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttPubCompPacket : MqttBasePublishPacket { + #region Added in MQTTv5 + + public MqttPubCompReasonCode? ReasonCode { get; set; } + + public MqttPubCompPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "PubComp"; + return string.Concat("PubComp: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs new file mode 100644 index 00000000..c29ad6a6 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttPubCompPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttPubRecPacket.cs b/Source/MQTTnet/Packets/MqttPubRecPacket.cs index 0184d6b8..e462ce8e 100644 --- a/Source/MQTTnet/Packets/MqttPubRecPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRecPacket.cs @@ -1,10 +1,20 @@ -namespace MQTTnet.Packets +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttPubRecPacket : MqttBasePublishPacket { + #region Added in MQTTv5 + + public MqttPubRecReasonCode? ReasonCode { get; set; } + + public MqttPubRecPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "PubRec"; + return string.Concat("PubRec: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs new file mode 100644 index 00000000..574bc617 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttPubRecPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttPubRelPacket.cs b/Source/MQTTnet/Packets/MqttPubRelPacket.cs index bb37655c..cf2c71b6 100644 --- a/Source/MQTTnet/Packets/MqttPubRelPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRelPacket.cs @@ -1,10 +1,20 @@ -namespace MQTTnet.Packets +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttPubRelPacket : MqttBasePublishPacket { + #region Added in MQTTv5 + + public MqttPubRelReasonCode? ReasonCode { get; set; } + + public MqttPubRelPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "PubRel"; + return string.Concat("PubRel: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs new file mode 100644 index 00000000..d7a21f64 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttPubRelPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttPublishPacket.cs b/Source/MQTTnet/Packets/MqttPublishPacket.cs index 55144a9e..2f20b143 100644 --- a/Source/MQTTnet/Packets/MqttPublishPacket.cs +++ b/Source/MQTTnet/Packets/MqttPublishPacket.cs @@ -14,14 +14,15 @@ namespace MQTTnet.Packets public byte[] Payload { get; set; } + #region Added in MQTTv5 + + public MqttPublishPacketProperties Properties { get; set; } + + #endregion + public override string ToString() { - return "Publish: [Topic=" + Topic + "]" + - " [Payload.Length=" + Payload?.Length + "]" + - " [QoSLevel=" + QualityOfServiceLevel + "]" + - " [Dup=" + Dup + "]" + - " [Retain=" + Retain + "]" + - " [PacketIdentifier=" + PacketIdentifier + "]"; + return string.Concat("Publish: [Topic=", Topic, "] [Payload.Length=", Payload?.Length, "] [QoSLevel=", QualityOfServiceLevel, "] [Dup=", Dup, "] [Retain=", Retain, "] [PacketIdentifier=", PacketIdentifier, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs b/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs new file mode 100644 index 00000000..0de4d812 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public class MqttPublishPacketProperties + { + public MqttPayloadFormatIndicator? PayloadFormatIndicator { get; set; } + + public uint? MessageExpiryInterval { get; set; } + + public ushort? TopicAlias { get; set; } + + public string ResponseTopic { get; set; } + + public byte[] CorrelationData { get; set; } + + public List UserProperties { get; } = new List(); + + public uint? SubscriptionIdentifier { get; set; } + + public string ContentType { get; set; } + } +} diff --git a/Source/MQTTnet/Packets/MqttSubAckPacket.cs b/Source/MQTTnet/Packets/MqttSubAckPacket.cs index 05e18b2b..053e7137 100644 --- a/Source/MQTTnet/Packets/MqttSubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttSubAckPacket.cs @@ -8,12 +8,22 @@ namespace MQTTnet.Packets { public ushort? PacketIdentifier { get; set; } - public IList SubscribeReturnCodes { get; } = new List(); + public List ReturnCodes { get; } = new List(); + + #region Added in MQTTv5.0.0 + + public List ReasonCodes { get; } = new List(); + + public MqttSubAckPacketProperties Properties { get; set; } + + #endregion public override string ToString() { - var subscribeReturnCodesText = string.Join(",", SubscribeReturnCodes.Select(f => f.ToString())); - return "SubAck: [PacketIdentifier=" + PacketIdentifier + "] [SubscribeReturnCodes=" + subscribeReturnCodesText + "]"; + var returnCodesText = string.Join(",", ReturnCodes.Select(f => f.ToString())); + var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); + + return string.Concat("SubAck: [PacketIdentifier=", PacketIdentifier, "] [ReturnCodes=", returnCodesText, "] [ReasonCode=", reasonCodesText, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs new file mode 100644 index 00000000..b2b3eff5 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttSubAckPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttSubscribePacket.cs b/Source/MQTTnet/Packets/MqttSubscribePacket.cs index 60086c47..c96c75bc 100644 --- a/Source/MQTTnet/Packets/MqttSubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttSubscribePacket.cs @@ -7,12 +7,18 @@ namespace MQTTnet.Packets { public ushort? PacketIdentifier { get; set; } - public IList TopicFilters { get; set; } = new List(); + public List TopicFilters { get; } = new List(); + + #region Added in MQTTv5 + + public MqttSubscribePacketProperties Properties { get; set; } + + #endregion public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters.Select(f => f.Topic + "@" + f.QualityOfServiceLevel)); - return "Subscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; + return string.Concat("Subscribe: [PacketIdentifier=", PacketIdentifier, "] [TopicFilters=", topicFiltersText, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs b/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs new file mode 100644 index 00000000..5b92c5f5 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttSubscribePacketProperties + { + public uint? SubscriptionIdentifier { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs index ab5f9f62..0b45df67 100644 --- a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs @@ -1,12 +1,26 @@ -namespace MQTTnet.Packets +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { public class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier { public ushort? PacketIdentifier { get; set; } + #region Added in MQTTv5 + + public MqttUnsubAckPacketProperties Properties { get; set; } + + public List ReasonCodes { get; } = new List(); + + #endregion + public override string ToString() { - return "UnsubAck: [PacketIdentifier=" + PacketIdentifier + "]"; + var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); + + return string.Concat("UnsubAck: [PacketIdentifier=", PacketIdentifier, "] [ReasonCodes=", reasonCodesText, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs new file mode 100644 index 00000000..ca444d9e --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttUnsubAckPacketProperties + { + public string ReasonString { get; set; } + + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttUnsubscribe.cs b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs similarity index 51% rename from Source/MQTTnet/Packets/MqttUnsubscribe.cs rename to Source/MQTTnet/Packets/MqttUnsubscribePacket.cs index 63895f12..bb1d4c2a 100644 --- a/Source/MQTTnet/Packets/MqttUnsubscribe.cs +++ b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs @@ -6,12 +6,18 @@ namespace MQTTnet.Packets { public ushort? PacketIdentifier { get; set; } - public IList TopicFilters { get; set; } = new List(); + public List TopicFilters { get; } = new List(); + + #region Added in MQTTv5 + + public MqttUnsubscribePacketProperties Properties { get; set; } + + #endregion public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters); - return "Unsubscribe: [PacketIdentifier=" + PacketIdentifier + "] [TopicFilters=" + topicFiltersText + "]"; + return string.Concat("Unsubscribe: [PacketIdentifier=", PacketIdentifier, "] [TopicFilters=", topicFiltersText, "]"); } } } diff --git a/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs b/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs new file mode 100644 index 00000000..5fb4d66b --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MQTTnet.Packets +{ + public class MqttUnsubscribePacketProperties + { + public List UserProperties { get; } = new List(); + } +} diff --git a/Source/MQTTnet/Packets/MqttUserProperty.cs b/Source/MQTTnet/Packets/MqttUserProperty.cs new file mode 100644 index 00000000..eaf9eae3 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUserProperty.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Packets +{ + public 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; } + } +} diff --git a/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs new file mode 100644 index 00000000..48d79481 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs @@ -0,0 +1,9 @@ +namespace MQTTnet.Protocol +{ + public enum MqttAuthenticateReasonCode + { + Success = 0, + ContinueAuthentication = 24, + ReAuthenticate = 25 + } +} diff --git a/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs new file mode 100644 index 00000000..cad4d77e --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs @@ -0,0 +1,28 @@ +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/MQTTnet/Protocol/MqttControlPacketType.cs b/Source/MQTTnet/Protocol/MqttControlPacketType.cs index adb58b66..12dce634 100644 --- a/Source/MQTTnet/Protocol/MqttControlPacketType.cs +++ b/Source/MQTTnet/Protocol/MqttControlPacketType.cs @@ -15,6 +15,7 @@ UnsubAck = 11, PingReq = 12, PingResp = 13, - Disconnect = 14 + Disconnect = 14, + Auth = 15 } } diff --git a/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs new file mode 100644 index 00000000..c1d2e3dc --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs @@ -0,0 +1,35 @@ +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/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs new file mode 100644 index 00000000..bf776a11 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs @@ -0,0 +1,8 @@ +namespace MQTTnet.Protocol +{ + public enum MqttPayloadFormatIndicator + { + Unspecified = 0, + CharacterData = 1 + } +} diff --git a/Source/MQTTnet/Protocol/MqttPropertyID.cs b/Source/MQTTnet/Protocol/MqttPropertyID.cs new file mode 100644 index 00000000..2e93a158 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPropertyID.cs @@ -0,0 +1,33 @@ +namespace MQTTnet.Protocol +{ + public enum MqttPropertyId + { + PayloadFormatIndicator = 1, + MessageExpiryInterval = 2, + ContentType = 3, + ResponseTopic = 8, + CorrelationData = 9, + SubscriptionIdentifier = 11, + SessionExpiryInterval = 17, + AssignedClientIdentifer = 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/MQTTnet/Protocol/MqttPubAckReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs new file mode 100644 index 00000000..b0580d6a --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs @@ -0,0 +1,15 @@ +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/MQTTnet/Protocol/MqttPubCompReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs new file mode 100644 index 00000000..9c23927b --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs @@ -0,0 +1,8 @@ +namespace MQTTnet.Protocol +{ + public enum MqttPubCompReasonCode + { + Success = 0, + PacketIdentifierNotFound = 146 + } +} diff --git a/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs new file mode 100644 index 00000000..b64eac23 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs @@ -0,0 +1,15 @@ +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/MQTTnet/Protocol/MqttPubRelReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs new file mode 100644 index 00000000..d71da680 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs @@ -0,0 +1,8 @@ +namespace MQTTnet.Protocol +{ + public enum MqttPubRelReasonCode + { + Success = 0, + PacketIdentifierNotFound = 146 + } +} diff --git a/Source/MQTTnet/Protocol/MqttRetainHandling.cs b/Source/MQTTnet/Protocol/MqttRetainHandling.cs new file mode 100644 index 00000000..8ee50ccd --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttRetainHandling.cs @@ -0,0 +1,9 @@ +namespace MQTTnet.Protocol +{ + public enum MqttRetainHandling + { + SendAtSubscribe = 0, + SendAtSubscribeIfNewSubscriptionOnly = 1, + DoNotSendOnSubscribe = 2 + } +} diff --git a/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs new file mode 100644 index 00000000..e3f2c0f9 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs @@ -0,0 +1,18 @@ +namespace MQTTnet.Protocol +{ + public enum MqttSubscribeReasonCode + { + GrantedQoS0 = 0, + GrantedQoS1 = 1, + GrantedQoS2 = 2, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + SharedSubscriptionsNotSupported = 158, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/MQTTnet/Protocol/MqttTopicValidator.cs b/Source/MQTTnet/Protocol/MqttTopicValidator.cs new file mode 100644 index 00000000..8bfda6e9 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttTopicValidator.cs @@ -0,0 +1,25 @@ +using MQTTnet.Exceptions; + +namespace MQTTnet.Protocol +{ + public static class MqttTopicValidator + { + public static void ThrowIfInvalid(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + throw new MqttProtocolViolationException("Topic should not be empty."); + } + + if (topic.Contains("+")) + { + throw new MqttProtocolViolationException("The character '+' is not allowed in topics."); + } + + if (topic.Contains("#")) + { + throw new MqttProtocolViolationException("The character '#' is not allowed in topics."); + } + } + } +} diff --git a/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs new file mode 100644 index 00000000..af062831 --- /dev/null +++ b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs @@ -0,0 +1,13 @@ +namespace MQTTnet.Protocol +{ + public enum MqttUnsubscribeReasonCode + { + Success = 0, + NoSubscriptionExisted = 17, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145 + } +} diff --git a/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs b/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs deleted file mode 100644 index e3066f9f..00000000 --- a/Source/MQTTnet/Serializer/IMqttPacketSerializer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using MQTTnet.Adapter; -using MQTTnet.Packets; - -namespace MQTTnet.Serializer -{ - public interface IMqttPacketSerializer - { - MqttProtocolVersion ProtocolVersion { get; set; } - - ArraySegment Serialize(MqttBasePacket mqttPacket); - - MqttBasePacket Deserialize(ReceivedMqttPacket receivedMqttPacket); - - void FreeBuffer(); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs b/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs deleted file mode 100644 index f0e1b562..00000000 --- a/Source/MQTTnet/Serializer/MqttPacketBodyReader.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Text; - -namespace MQTTnet.Serializer -{ - public class MqttPacketBodyReader - { - private readonly byte[] _buffer; - private readonly int _length; - - private int _offset; - - public MqttPacketBodyReader(byte[] buffer, int offset, int length) - { - _buffer = buffer; - _offset = offset; - _length = length; - } - - public int Length => _length - _offset; - - public bool EndOfStream => _offset == _length; - - public byte ReadByte() - { - ValidateReceiveBuffer(1); - return _buffer[_offset++]; - } - - public ArraySegment ReadRemainingData() - { - return new ArraySegment(_buffer, _offset, _length - _offset); - } - - public ushort ReadUInt16() - { - ValidateReceiveBuffer(2); - - var msb = _buffer[_offset++]; - var lsb = _buffer[_offset++]; - - return (ushort)(msb << 8 | lsb); - } - - public ArraySegment ReadWithLengthPrefix() - { - var length = ReadUInt16(); - - ValidateReceiveBuffer(length); - - var result = new ArraySegment(_buffer, _offset, length); - _offset += length; - - return result; - } - - private void ValidateReceiveBuffer(ushort length) - { - if (_length < _offset + length) - { - throw new ArgumentOutOfRangeException(nameof(_buffer), $"expected at least {_offset + length} bytes but there are only {_length} bytes"); - } - } - - public string ReadStringWithLengthPrefix() - { - var buffer = ReadWithLengthPrefix(); - return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - } - } -} diff --git a/Source/MQTTnet/Serializer/MqttPacketReader.cs b/Source/MQTTnet/Serializer/MqttPacketReader.cs deleted file mode 100644 index f115b507..00000000 --- a/Source/MQTTnet/Serializer/MqttPacketReader.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Channel; -using MQTTnet.Exceptions; -using MQTTnet.Internal; - -namespace MQTTnet.Serializer -{ - public static class MqttPacketReader - { - public static async Task ReadFixedHeaderAsync(IMqttChannel channel, byte[] fixedHeaderBuffer, byte[] singleByteBuffer, 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. - // async/await is used here because the next packet is received in a couple of minutes so the performance - // impact is acceptable according to a useless waiting thread. - var buffer = fixedHeaderBuffer; - var totalBytesRead = 0; - - while (totalBytesRead < buffer.Length) - { - var bytesRead = await channel.ReadAsync(buffer, totalBytesRead, buffer.Length - totalBytesRead, cancellationToken).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - ExceptionHelper.ThrowIfGracefulSocketClose(bytesRead); - - totalBytesRead += bytesRead; - } - - var hasRemainingLength = buffer[1] != 0; - if (!hasRemainingLength) - { - return new MqttFixedHeader(buffer[0], 0); - } - -#if WINDOWS_UWP - // UWP will have a dead lock when calling this not async. - var bodyLength = await ReadBodyLengthAsync(channel, buffer[1], singleByteBuffer, cancellationToken).ConfigureAwait(false); -#else - // Here the async/await pattern is not used becuase the overhead of context switches - // is too big for reading 1 byte in a row. We expect that the remaining data was sent - // directly after the initial bytes. If the client disconnects just in this moment we - // will get an exception anyway. - var bodyLength = ReadBodyLength(channel, buffer[1], singleByteBuffer, cancellationToken); -#endif - - return new MqttFixedHeader(buffer[0], bodyLength); - } - -#if !WINDOWS_UWP - private static int ReadBodyLength(IMqttChannel channel, byte initialEncodedByte, byte[] singleByteBuffer, 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."); - } - - cancellationToken.ThrowIfCancellationRequested(); - - encodedByte = ReadByte(channel, singleByteBuffer, cancellationToken); - - value += (byte)(encodedByte & 127) * multiplier; - multiplier *= 128; - } - - return value; - } - - private static byte ReadByte(IMqttChannel channel, byte[] singleByteBuffer, CancellationToken cancellationToken) - { - var readCount = channel.ReadAsync(singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); - - cancellationToken.ThrowIfCancellationRequested(); - - if (readCount <= 0) - { - ExceptionHelper.ThrowGracefulSocketClose(); - } - - return singleByteBuffer[0]; - } - -#else - - private static async Task ReadBodyLengthAsync(IMqttChannel channel, byte initialEncodedByte, byte[] singleByteBuffer, 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."); - } - - cancellationToken.ThrowIfCancellationRequested(); - - encodedByte = await ReadByteAsync(channel, singleByteBuffer, cancellationToken).ConfigureAwait(false); - - value += (byte)(encodedByte & 127) * multiplier; - multiplier *= 128; - } - - return value; - } - - private static async Task ReadByteAsync(IMqttChannel channel, byte[] singleByteBuffer, CancellationToken cancellationToken) - { - var readCount = await channel.ReadAsync(singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - if (readCount <= 0) - { - ExceptionHelper.ThrowGracefulSocketClose(); - } - - return singleByteBuffer[0]; - } - -#endif - } -} diff --git a/Source/MQTTnet/Serializer/MqttPacketWriter.cs b/Source/MQTTnet/Serializer/MqttPacketWriter.cs deleted file mode 100644 index 67fd4672..00000000 --- a/Source/MQTTnet/Serializer/MqttPacketWriter.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Text; -using MQTTnet.Protocol; - -namespace MQTTnet.Serializer -{ - /// - /// 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 class MqttPacketWriter - { - public static int MaxBufferSize = 4096; - - private byte[] _buffer = new byte[128]; - - private int _position; - - public int Length { get; private set; } - - public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) - { - var fixedHeader = (int)packetType << 4; - fixedHeader |= flags; - return (byte)fixedHeader; - } - - public static ArraySegment EncodeRemainingLength(int length) - { - if (length <= 0) - { - return new ArraySegment(new byte[1], 0, 1); - } - - var buffer = new byte[4]; - var bufferOffset = 0; - - var x = length; - do - { - var encodedByte = x % 128; - x = x / 128; - if (x > 0) - { - encodedByte = encodedByte | 128; - } - - buffer[bufferOffset] = (byte)encodedByte; - bufferOffset++; - } while (x > 0); - - return new ArraySegment(buffer, 0, bufferOffset); - } - - public void WriteWithLengthPrefix(string value) - { - WriteWithLengthPrefix(Encoding.UTF8.GetBytes(value ?? string.Empty)); - } - - public void WriteWithLengthPrefix(byte[] value) - { - EnsureAdditionalCapacity(value.Length + 2); - - Write((ushort)value.Length); - Write(value, 0, value.Length); - } - - public void Write(byte @byte) - { - EnsureAdditionalCapacity(1); - - _buffer[_position] = @byte; - IncreasePostition(1); - } - - public void Write(ushort value) - { - EnsureAdditionalCapacity(2); - - _buffer[_position] = (byte)(value >> 8); - IncreasePostition(1); - _buffer[_position] = (byte)value; - IncreasePostition(1); - } - - public void Write(byte[] array, int offset, int count) - { - if (array == null) throw new ArgumentNullException(nameof(array)); - - EnsureAdditionalCapacity(count); - - Array.Copy(array, offset, _buffer, _position, count); - IncreasePostition(count); - } - - public void Reset() - { - Length = 5; - } - - public void Seek(int offset) - { - EnsureCapacity(offset); - _position = offset; - } - - public byte[] GetBuffer() - { - return _buffer; - } - - public void FreeBuffer() - { - // This method frees the used memory by shrinking the buffer. This is required because the buffer - // is used across several messages. In general this is not a big issue because subsequent Ping packages - // have the same size but a very big publish package with 100 MB of payload will increase the buffer - // a lot and the size will never reduced. So this method tries to find a size which can be held in - // memory for a long time without causing troubles. - - if (_buffer.Length < MaxBufferSize) - { - return; - } - - Array.Resize(ref _buffer, MaxBufferSize); - } - - private void EnsureAdditionalCapacity(int additionalCapacity) - { - var freeSpace = _buffer.Length - _position; - if (freeSpace >= additionalCapacity) - { - return; - } - - EnsureCapacity(_buffer.Length + additionalCapacity - freeSpace); - } - - private void EnsureCapacity(int capacity) - { - var newBufferLength = _buffer.Length; - - if (newBufferLength >= capacity) - { - return; - } - - while (newBufferLength < capacity) - { - newBufferLength *= 2; - } - - Array.Resize(ref _buffer, newBufferLength); - } - - private void IncreasePostition(int length) - { - _position += length; - - if (_position > Length) - { - Length = _position; - } - } - } -} diff --git a/Source/MQTTnet/Serializer/MqttProtocolVersion.cs b/Source/MQTTnet/Serializer/MqttProtocolVersion.cs deleted file mode 100644 index 15e698d3..00000000 --- a/Source/MQTTnet/Serializer/MqttProtocolVersion.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MQTTnet.Serializer -{ - public enum MqttProtocolVersion - { - V311, - V310 - } -} diff --git a/Source/MQTTnet/Server/IMqttClientSession.cs b/Source/MQTTnet/Server/IMqttClientSession.cs index 9fdb0eb9..d7db2b13 100644 --- a/Source/MQTTnet/Server/IMqttClientSession.cs +++ b/Source/MQTTnet/Server/IMqttClientSession.cs @@ -1,23 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Packets; +using System.Threading.Tasks; namespace MQTTnet.Server { - public interface IMqttClientSession : IDisposable + public interface IMqttClientSession { string ClientId { get; } - void FillStatus(MqttClientSessionStatus status); - void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket); - void ClearPendingApplicationMessages(); - - Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter); - void Stop(MqttClientDisconnectType disconnectType); - - Task SubscribeAsync(IList topicFilters); - Task UnsubscribeAsync(IList topicFilters); + Task StopAsync(); } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttClientSessionStatus.cs b/Source/MQTTnet/Server/IMqttClientSessionStatus.cs deleted file mode 100644 index d9c55938..00000000 --- a/Source/MQTTnet/Server/IMqttClientSessionStatus.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Serializer; - -namespace MQTTnet.Server -{ - public interface IMqttClientSessionStatus - { - string ClientId { get; } - - string Endpoint { get; } - - bool IsConnected { get; } - - MqttProtocolVersion? ProtocolVersion { get; } - - TimeSpan LastPacketReceived { get; } - - TimeSpan LastNonKeepAlivePacketReceived { get; } - - int PendingApplicationMessagesCount { get; } - - Task DisconnectAsync(); - - Task DeleteSessionAsync(); - - Task ClearPendingApplicationMessagesAsync(); - } -} diff --git a/Source/MQTTnet/Server/IMqttServer.cs b/Source/MQTTnet/Server/IMqttServer.cs index 3a45973b..c080f4e8 100644 --- a/Source/MQTTnet/Server/IMqttServer.cs +++ b/Source/MQTTnet/Server/IMqttServer.cs @@ -1,31 +1,29 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using MQTTnet.Server.Status; namespace MQTTnet.Server { public interface IMqttServer : IApplicationMessageReceiver, IApplicationMessagePublisher { - event EventHandler Started; - event EventHandler Stopped; + IMqttServerStartedHandler StartedHandler { get; set; } + IMqttServerStoppedHandler StoppedHandler { get; set; } - event EventHandler ClientConnected; - event EventHandler ClientDisconnected; - event EventHandler ClientSubscribedTopic; - event EventHandler ClientUnsubscribedTopic; + IMqttServerClientConnectedHandler ClientConnectedHandler { get; set; } + IMqttServerClientDisconnectedHandler ClientDisconnectedHandler { get; set; } + IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler { get; set; } + IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler { get; set; } IMqttServerOptions Options { get; } - [Obsolete("This method is no longer async. Use the not async method.")] - Task> GetClientSessionsStatusAsync(); + Task> GetClientStatusAsync(); + Task> GetSessionStatusAsync(); - IList GetClientSessionsStatus(); + Task> GetRetainedApplicationMessagesAsync(); + Task ClearRetainedApplicationMessagesAsync(); - IList GetRetainedMessages(); - Task ClearRetainedMessagesAsync(); - - Task SubscribeAsync(string clientId, IList topicFilters); - Task UnsubscribeAsync(string clientId, IList topicFilters); + Task SubscribeAsync(string clientId, ICollection topicFilters); + Task UnsubscribeAsync(string clientId, ICollection topicFilters); Task StartAsync(IMqttServerOptions options); Task StopAsync(); diff --git a/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs b/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs new file mode 100644 index 00000000..64fe0471 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerApplicationMessageInterceptor + { + Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs b/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs new file mode 100644 index 00000000..b507e41b --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerClientConnectedHandler + { + Task HandleClientConnectedAsync(MqttServerClientConnectedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs b/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs new file mode 100644 index 00000000..3614d2d2 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerClientDisconnectedHandler + { + Task HandleClientDisconnectedAsync(MqttServerClientDisconnectedEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs b/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs new file mode 100644 index 00000000..c6f97a73 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerClientMessageQueueInterceptor + { + Task InterceptClientMessageQueueEnqueueAsync(MqttClientMessageQueueInterceptorContext context); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs b/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs new file mode 100644 index 00000000..6f82bb36 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerClientSubscribedTopicHandler + { + Task HandleClientSubscribedTopicAsync(MqttServerClientSubscribedTopicEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs b/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs new file mode 100644 index 00000000..077a80f9 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerClientUnsubscribedTopicHandler + { + Task HandleClientUnsubscribedTopicAsync(MqttServerClientUnsubscribedTopicEventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs b/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs new file mode 100644 index 00000000..68b5be6f --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerConnectionValidator + { + Task ValidateConnectionAsync(MqttConnectionValidatorContext context); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerFactory.cs b/Source/MQTTnet/Server/IMqttServerFactory.cs index 7d23ed36..e8c7d401 100644 --- a/Source/MQTTnet/Server/IMqttServerFactory.cs +++ b/Source/MQTTnet/Server/IMqttServerFactory.cs @@ -10,6 +10,8 @@ namespace MQTTnet.Server IMqttServer CreateMqttServer(IMqttNetLogger logger); + IMqttServer CreateMqttServer(IEnumerable adapters); + IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger); } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerOptions.cs b/Source/MQTTnet/Server/IMqttServerOptions.cs index f9334fd7..3a242895 100644 --- a/Source/MQTTnet/Server/IMqttServerOptions.cs +++ b/Source/MQTTnet/Server/IMqttServerOptions.cs @@ -4,6 +4,8 @@ namespace MQTTnet.Server { public interface IMqttServerOptions { + string ClientId { get; set; } + bool EnablePersistentSessions { get; } int MaxPendingMessagesPerClient { get; } @@ -11,14 +13,14 @@ namespace MQTTnet.Server TimeSpan DefaultCommunicationTimeout { get; } - Action ConnectionValidator { get; } - Action SubscriptionInterceptor { get; } - Action ApplicationMessageInterceptor { get; } - Action ClientMessageQueueInterceptor { get; } + IMqttServerConnectionValidator ConnectionValidator { get; } + IMqttServerSubscriptionInterceptor SubscriptionInterceptor { get; } + IMqttServerApplicationMessageInterceptor ApplicationMessageInterceptor { get; } + IMqttServerClientMessageQueueInterceptor ClientMessageQueueInterceptor { get; } MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } - IMqttServerStorage Storage { get; } + IMqttServerStorage Storage { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerStartedHandler.cs b/Source/MQTTnet/Server/IMqttServerStartedHandler.cs new file mode 100644 index 00000000..e6155f14 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerStartedHandler.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerStartedHandler + { + Task HandleServerStartedAsync(EventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs b/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs new file mode 100644 index 00000000..34df3236 --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerStoppedHandler + { + Task HandleServerStoppedAsync(EventArgs eventArgs); + } +} diff --git a/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs b/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs new file mode 100644 index 00000000..a7ce95ef --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public interface IMqttServerSubscriptionInterceptor + { + Task InterceptSubscriptionAsync(MqttSubscriptionInterceptorContext context); + } +} diff --git a/Source/MQTTnet/Server/MqttClientConnection.cs b/Source/MQTTnet/Server/MqttClientConnection.cs new file mode 100644 index 00000000..4d870d55 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientConnection.cs @@ -0,0 +1,481 @@ +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.PacketDispatcher; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server.Status; + +namespace MQTTnet.Server +{ + public class MqttClientConnection : IMqttClientSession, IDisposable + { + private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); + private readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); + private readonly CancellationTokenSource _cancellationToken = new CancellationTokenSource(); + + private readonly MqttRetainedMessagesManager _retainedMessagesManager; + private readonly MqttClientKeepAliveMonitor _keepAliveMonitor; + private readonly MqttClientSessionsManager _sessionsManager; + + private readonly IMqttNetChildLogger _logger; + private readonly IMqttServerOptions _serverOptions; + + private readonly IMqttChannelAdapter _channelAdapter; + private readonly IMqttDataConverter _dataConverter; + private readonly string _endpoint; + private readonly MqttConnectPacket _connectPacket; + + private Task _packageReceiverTask; + private DateTime _lastPacketReceivedTimestamp; + private DateTime _lastNonKeepAlivePacketReceivedTimestamp; + + private long _receivedPacketsCount; + private long _sentPacketsCount = 1; // Start with 1 because the CONNECT packet is not counted anywhere. + private long _receivedApplicationMessagesCount; + private long _sentApplicationMessagesCount; + + public MqttClientConnection( + MqttConnectPacket connectPacket, + IMqttChannelAdapter channelAdapter, + MqttClientSession session, + IMqttServerOptions serverOptions, + MqttClientSessionsManager sessionsManager, + MqttRetainedMessagesManager retainedMessagesManager, + IMqttNetChildLogger logger) + { + Session = session ?? throw new ArgumentNullException(nameof(session)); + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + + _channelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); + _dataConverter = _channelAdapter.PacketFormatterAdapter.DataConverter; + _endpoint = _channelAdapter.Endpoint; + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.CreateChildLogger(nameof(MqttClientConnection)); + + _keepAliveMonitor = new MqttClientKeepAliveMonitor(this, _logger); + + _lastPacketReceivedTimestamp = DateTime.UtcNow; + _lastNonKeepAlivePacketReceivedTimestamp = _lastPacketReceivedTimestamp; + } + + public string ClientId => _connectPacket.ClientId; + + public MqttClientSession Session { get; } + + public async Task StopAsync() + { + StopInternal(); + + var task = _packageReceiverTask; + if (task != null) + { + await task.ConfigureAwait(false); + } + } + + public void FillStatus(MqttClientStatus status) + { + status.ClientId = ClientId; + status.Endpoint = _endpoint; + status.ProtocolVersion = _channelAdapter.PacketFormatterAdapter.ProtocolVersion; + + status.ReceivedApplicationMessagesCount = Interlocked.Read(ref _receivedApplicationMessagesCount); + status.SentApplicationMessagesCount = Interlocked.Read(ref _sentApplicationMessagesCount); + + status.ReceivedPacketsCount = Interlocked.Read(ref _receivedPacketsCount); + status.SentPacketsCount = Interlocked.Read(ref _sentPacketsCount); + + status.LastPacketReceivedTimestamp = _lastPacketReceivedTimestamp; + status.LastNonKeepAlivePacketReceivedTimestamp = _lastNonKeepAlivePacketReceivedTimestamp; + + status.BytesSent = _channelAdapter.BytesSent; + status.BytesReceived = _channelAdapter.BytesReceived; + } + + public void Dispose() + { + _cancellationToken.Dispose(); + } + + public Task RunAsync() + { + _packageReceiverTask = RunInternalAsync(); + return _packageReceiverTask; + } + + private async Task RunInternalAsync() + { + var disconnectType = MqttClientDisconnectType.NotClean; + try + { + _logger.Info("Client '{0}': Session started.", ClientId); + + _channelAdapter.ReadingPacketStartedCallback = OnAdapterReadingPacketStarted; + _channelAdapter.ReadingPacketCompletedCallback = OnAdapterReadingPacketCompleted; + + Session.WillMessage = _connectPacket.WillMessage; + +#pragma warning disable 4014 + Task.Run(() => SendPendingPacketsAsync(_cancellationToken.Token), _cancellationToken.Token); +#pragma warning restore 4014 + + // TODO: Change to single thread in SessionManager. Or use SessionManager and stats from KeepAliveMonitor. + _keepAliveMonitor.Start(_connectPacket.KeepAlivePeriod, _cancellationToken.Token); + + await SendAsync( + new MqttConnAckPacket + { + ReturnCode = MqttConnectReturnCode.ConnectionAccepted, + ReasonCode = MqttConnectReasonCode.Success, + IsSessionPresent = Session.IsCleanSession + }).ConfigureAwait(false); + + Session.IsCleanSession = false; + + while (!_cancellationToken.IsCancellationRequested) + { + var packet = await _channelAdapter.ReceivePacketAsync(TimeSpan.Zero, _cancellationToken.Token).ConfigureAwait(false); + if (packet == null) + { + // The client has closed the connection gracefully. + break; + } + + Interlocked.Increment(ref _sentPacketsCount); + _lastPacketReceivedTimestamp = DateTime.UtcNow; + + if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket)) + { + _lastNonKeepAlivePacketReceivedTimestamp = _lastPacketReceivedTimestamp; + } + + _keepAliveMonitor.PacketReceived(); + + if (packet is MqttPublishPacket publishPacket) + { + await HandleIncomingPublishPacketAsync(publishPacket).ConfigureAwait(false); + continue; + } + + if (packet is MqttPubRelPacket pubRelPacket) + { + var pubCompPacket = new MqttPubCompPacket + { + PacketIdentifier = pubRelPacket.PacketIdentifier, + ReasonCode = MqttPubCompReasonCode.Success + }; + + await SendAsync(pubCompPacket).ConfigureAwait(false); + continue; + } + + if (packet is MqttSubscribePacket subscribePacket) + { + await HandleIncomingSubscribePacketAsync(subscribePacket).ConfigureAwait(false); + continue; + } + + if (packet is MqttUnsubscribePacket unsubscribePacket) + { + await HandleIncomingUnsubscribePacketAsync(unsubscribePacket).ConfigureAwait(false); + continue; + } + + if (packet is MqttPingReqPacket) + { + await SendAsync(new MqttPingRespPacket()).ConfigureAwait(false); + continue; + } + + if (packet is MqttDisconnectPacket) + { + Session.WillMessage = null; + disconnectType = MqttClientDisconnectType.Clean; + + StopInternal(); + break; + } + + _packetDispatcher.Dispatch(packet); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Client '{0}': Communication exception while receiving client packets.", ClientId); + } + else + { + _logger.Error(exception, "Client '{0}': Unhandled exception while receiving client packets.", ClientId); + } + + StopInternal(); + } + finally + { + if (Session.WillMessage != null) + { + _sessionsManager.DispatchApplicationMessage(Session.WillMessage, this); + Session.WillMessage = null; + } + + _packetDispatcher.Reset(); + + _channelAdapter.ReadingPacketStartedCallback = null; + _channelAdapter.ReadingPacketCompletedCallback = null; + + _logger.Info("Client '{0}': Session stopped.", ClientId); + + _packageReceiverTask = null; + } + + return disconnectType; + } + + private void StopInternal() + { + _cancellationToken.Cancel(false); + } + + private async Task EnqueueSubscribedRetainedMessagesAsync(ICollection topicFilters) + { + var retainedMessages = await _retainedMessagesManager.GetSubscribedMessagesAsync(topicFilters).ConfigureAwait(false); + foreach (var applicationMessage in retainedMessages) + { + Session.EnqueueApplicationMessage(applicationMessage, ClientId, true); + } + } + + private async Task HandleIncomingSubscribePacketAsync(MqttSubscribePacket subscribePacket) + { + // TODO: Let the channel adapter create the packet. + var subscribeResult = await Session.SubscriptionsManager.SubscribeAsync(subscribePacket).ConfigureAwait(false); + + await SendAsync(subscribeResult.ResponsePacket).ConfigureAwait(false); + + if (subscribeResult.CloseConnection) + { + StopInternal(); + return; + } + + await EnqueueSubscribedRetainedMessagesAsync(subscribePacket.TopicFilters).ConfigureAwait(false); + } + + private async Task HandleIncomingUnsubscribePacketAsync(MqttUnsubscribePacket unsubscribePacket) + { + // TODO: Let the channel adapter create the packet. + var unsubscribeResult = await Session.SubscriptionsManager.UnsubscribeAsync(unsubscribePacket).ConfigureAwait(false); + await SendAsync(unsubscribeResult).ConfigureAwait(false); + } + + private Task HandleIncomingPublishPacketAsync(MqttPublishPacket publishPacket) + { + Interlocked.Increment(ref _sentApplicationMessagesCount); + + switch (publishPacket.QualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + return HandleIncomingPublishPacketWithQoS0Async(publishPacket); + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + return HandleIncomingPublishPacketWithQoS1Async(publishPacket); + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + return HandleIncomingPublishPacketWithQoS2Async(publishPacket); + } + default: + { + throw new MqttCommunicationException("Received a not supported QoS level."); + } + } + } + + private Task HandleIncomingPublishPacketWithQoS0Async(MqttPublishPacket publishPacket) + { + var applicationMessage = _dataConverter.CreateApplicationMessage(publishPacket); + + _sessionsManager.DispatchApplicationMessage(applicationMessage, this); + + return Task.FromResult(0); + } + + private Task HandleIncomingPublishPacketWithQoS1Async(MqttPublishPacket publishPacket) + { + var applicationMessage = _dataConverter.CreateApplicationMessage(publishPacket); + _sessionsManager.DispatchApplicationMessage(applicationMessage, this); + + var pubAckPacket = _dataConverter.CreatePubAckPacket(publishPacket); + return SendAsync(pubAckPacket); + } + + private Task HandleIncomingPublishPacketWithQoS2Async(MqttPublishPacket publishPacket) + { + var applicationMessage = _dataConverter.CreateApplicationMessage(publishPacket); + _sessionsManager.DispatchApplicationMessage(applicationMessage, this); + + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubRecReasonCode.Success + }; + + return SendAsync(pubRecPacket); + } + + private async Task SendPendingPacketsAsync(CancellationToken cancellationToken) + { + MqttQueuedApplicationMessage queuedApplicationMessage = null; + MqttPublishPacket publishPacket = null; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + queuedApplicationMessage = await Session.ApplicationMessagesQueue.TakeAsync(cancellationToken).ConfigureAwait(false); + if (queuedApplicationMessage == null) + { + return; + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + publishPacket = _dataConverter.CreatePublishPacket(queuedApplicationMessage.ApplicationMessage); + publishPacket.QualityOfServiceLevel = queuedApplicationMessage.QualityOfServiceLevel; + + // Set the retain flag to true according to [MQTT-3.3.1-8] and [MQTT-3.3.1-9]. + publishPacket.Retain = queuedApplicationMessage.IsRetainedMessage; + + if (publishPacket.QualityOfServiceLevel > 0) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + } + + if (_serverOptions.ClientMessageQueueInterceptor != null) + { + var context = new MqttClientMessageQueueInterceptorContext( + queuedApplicationMessage.SenderClientId, + ClientId, + queuedApplicationMessage.ApplicationMessage); + + if (_serverOptions.ClientMessageQueueInterceptor != null) + { + await _serverOptions.ClientMessageQueueInterceptor.InterceptClientMessageQueueEnqueueAsync(context).ConfigureAwait(false); + } + + if (!context.AcceptEnqueue || context.ApplicationMessage == null) + { + return; + } + + publishPacket.Topic = context.ApplicationMessage.Topic; + publishPacket.Payload = context.ApplicationMessage.Payload; + publishPacket.QualityOfServiceLevel = context.ApplicationMessage.QualityOfServiceLevel; + } + + if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + await SendAsync(publishPacket).ConfigureAwait(false); + } + else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + var awaiter = _packetDispatcher.AddPacketAwaiter(publishPacket.PacketIdentifier); + await SendAsync(publishPacket).ConfigureAwait(false); + await awaiter.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); + } + else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + using (var awaiter1 = _packetDispatcher.AddPacketAwaiter(publishPacket.PacketIdentifier)) + using (var awaiter2 = _packetDispatcher.AddPacketAwaiter(publishPacket.PacketIdentifier)) + { + await SendAsync(publishPacket).ConfigureAwait(false); + await awaiter1.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); + + await SendAsync(new MqttPubRelPacket { PacketIdentifier = publishPacket.PacketIdentifier }).ConfigureAwait(false); + await awaiter2.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); + } + } + + _logger.Verbose("Queued application message sent (ClientId: {0}).", ClientId); + + // TODO: + //Interlocked.Increment(ref _sentPacketsCount); + } + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) + { + _logger.Warning(exception, "Sending publish packet failed: Timeout (ClientId: {0}).", ClientId); + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Sending publish packet failed: Communication exception (ClientId: {0}).", ClientId); + } + else if (exception is OperationCanceledException && _cancellationToken.Token.IsCancellationRequested) + { + // The cancellation was triggered externally. + } + else + { + _logger.Error(exception, "Sending publish packet failed (ClientId: {0}).", ClientId); + } + + if (publishPacket?.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + queuedApplicationMessage.IsDuplicate = true; + + Session.ApplicationMessagesQueue.Enqueue(queuedApplicationMessage); + } + + if (!_cancellationToken.Token.IsCancellationRequested) + { + await StopAsync().ConfigureAwait(false); + } + } + } + + private async Task SendAsync(MqttBasePacket packet) + { + await _channelAdapter.SendPacketAsync(packet, _serverOptions.DefaultCommunicationTimeout, _cancellationToken.Token).ConfigureAwait(false); + + Interlocked.Increment(ref _receivedPacketsCount); + + if (packet is MqttPublishPacket) + { + Interlocked.Increment(ref _receivedApplicationMessagesCount); + } + } + + private void OnAdapterReadingPacketCompleted() + { + _keepAliveMonitor?.Resume(); + } + + private void OnAdapterReadingPacketStarted() + { + _keepAliveMonitor?.Pause(); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientDisconnectType.cs b/Source/MQTTnet/Server/MqttClientDisconnectType.cs index 19e9da8d..c4d6f595 100644 --- a/Source/MQTTnet/Server/MqttClientDisconnectType.cs +++ b/Source/MQTTnet/Server/MqttClientDisconnectType.cs @@ -3,6 +3,7 @@ public enum MqttClientDisconnectType { Clean, - NotClean + NotClean, + Takeover } } diff --git a/Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs deleted file mode 100644 index 7e88548e..00000000 --- a/Source/MQTTnet/Server/MqttClientDisconnectedEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public class MqttClientDisconnectedEventArgs : EventArgs - { - public MqttClientDisconnectedEventArgs(string clientId, bool wasCleanDisconnect) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - WasCleanDisconnect = wasCleanDisconnect; - } - - public string ClientId { get; } - - public bool WasCleanDisconnect { get; } - } -} diff --git a/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs b/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs index fbe1f87f..0ca41abc 100644 --- a/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs +++ b/Source/MQTTnet/Server/MqttClientKeepAliveMonitor.cs @@ -3,14 +3,12 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Packets; namespace MQTTnet.Server { public class MqttClientKeepAliveMonitor { private readonly Stopwatch _lastPacketReceivedTracker = new Stopwatch(); - private readonly Stopwatch _lastNonKeepAlivePacketReceivedTracker = new Stopwatch(); private readonly IMqttClientSession _clientSession; private readonly IMqttNetChildLogger _logger; @@ -26,10 +24,6 @@ namespace MQTTnet.Server _logger = logger.CreateChildLogger(nameof(MqttClientKeepAliveMonitor)); } - public TimeSpan LastPacketReceived => _lastPacketReceivedTracker.Elapsed; - - public TimeSpan LastNonKeepAlivePacketReceived => _lastNonKeepAlivePacketReceivedTracker.Elapsed; - public void Start(int keepAlivePeriod, CancellationToken cancellationToken) { if (keepAlivePeriod == 0) @@ -50,20 +44,9 @@ namespace MQTTnet.Server _isPaused = false; } - public void Reset() + public void PacketReceived() { _lastPacketReceivedTracker.Restart(); - _lastNonKeepAlivePacketReceivedTracker.Restart(); - } - - public void PacketReceived(MqttBasePacket packet) - { - _lastPacketReceivedTracker.Restart(); - - if (!(packet is MqttPingReqPacket)) - { - _lastNonKeepAlivePacketReceivedTracker.Restart(); - } } private async Task RunAsync(int keepAlivePeriod, CancellationToken cancellationToken) @@ -71,7 +54,6 @@ namespace MQTTnet.Server try { _lastPacketReceivedTracker.Restart(); - _lastNonKeepAlivePacketReceivedTracker.Restart(); while (!cancellationToken.IsCancellationRequested) { @@ -81,7 +63,7 @@ namespace MQTTnet.Server if (!_isPaused && _lastPacketReceivedTracker.Elapsed.TotalSeconds >= keepAlivePeriod * 1.5D) { _logger.Warning(null, "Client '{0}': Did not receive any packet or keep alive signal.", _clientSession.ClientId); - _clientSession.Stop(MqttClientDisconnectType.NotClean); + await _clientSession.StopAsync().ConfigureAwait(false); return; } diff --git a/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs b/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs deleted file mode 100644 index 503d9926..00000000 --- a/Source/MQTTnet/Server/MqttClientPendingPacketsQueue.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Diagnostics; -using MQTTnet.Exceptions; -using MQTTnet.Internal; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Server -{ - public class MqttClientPendingPacketsQueue : IDisposable - { - private readonly Queue _queue = new Queue(); - private readonly AsyncAutoResetEvent _queueAutoResetEvent = new AsyncAutoResetEvent(); - - private readonly IMqttServerOptions _options; - private readonly MqttClientSession _clientSession; - private readonly IMqttNetChildLogger _logger; - - public MqttClientPendingPacketsQueue(IMqttServerOptions options, MqttClientSession clientSession, IMqttNetChildLogger logger) - { - if (logger == null) throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession)); - - _logger = logger.CreateChildLogger(nameof(MqttClientPendingPacketsQueue)); - } - - public int Count - { - get - { - lock (_queue) - { - return _queue.Count; - } - } - } - - public void Start(IMqttChannelAdapter adapter, CancellationToken cancellationToken) - { - if (adapter == null) throw new ArgumentNullException(nameof(adapter)); - - if (cancellationToken.IsCancellationRequested) - { - return; - } - - Task.Run(() => SendQueuedPacketsAsync(adapter, cancellationToken), cancellationToken); - } - - public void Enqueue(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - lock (_queue) - { - if (_queue.Count >= _options.MaxPendingMessagesPerClient) - { - if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) - { - return; - } - - if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) - { - _queue.Dequeue(); - } - } - - _queue.Enqueue(packet); - } - - _queueAutoResetEvent.Set(); - - _logger.Verbose("Enqueued packet (ClientId: {0}).", _clientSession.ClientId); - } - - public void Clear() - { - lock (_queue) - { - _queue.Clear(); - } - } - - public void Dispose() - { - } - - private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { - await TrySendNextQueuedPacketAsync(adapter, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - _logger.Error(exception, "Unhandled exception while sending enqueued packet (ClientId: {0}).", _clientSession.ClientId); - } - } - - private async Task TrySendNextQueuedPacketAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) - { - MqttBasePacket packet = null; - try - { - lock (_queue) - { - if (_queue.Count > 0) - { - packet = _queue.Dequeue(); - } - } - - if (packet == null) - { - await _queueAutoResetEvent.WaitOneAsync(cancellationToken).ConfigureAwait(false); - return; - } - - if (cancellationToken.IsCancellationRequested) - { - return; - } - - adapter.SendPacketAsync(packet, cancellationToken).GetAwaiter().GetResult(); - - _logger.Verbose("Enqueued packet sent (ClientId: {0}).", _clientSession.ClientId); - } - catch (Exception exception) - { - if (exception is MqttCommunicationTimedOutException) - { - _logger.Warning(exception, "Sending publish packet failed: Timeout (ClientId: {0}).", _clientSession.ClientId); - } - else if (exception is MqttCommunicationException) - { - _logger.Warning(exception, "Sending publish packet failed: Communication exception (ClientId: {0}).", _clientSession.ClientId); - } - else if (exception is OperationCanceledException) - { - } - else - { - _logger.Error(exception, "Sending publish packet failed (ClientId: {0}).", _clientSession.ClientId); - } - - if (packet is MqttPublishPacket publishPacket) - { - if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - publishPacket.Dup = true; - - Enqueue(publishPacket); - } - } - - if (!cancellationToken.IsCancellationRequested) - { - _clientSession.Stop(MqttClientDisconnectType.NotClean); - } - } - } - } -} diff --git a/Source/MQTTnet/Server/MqttClientSession.cs b/Source/MQTTnet/Server/MqttClientSession.cs index 7a1d6fec..804a2230 100644 --- a/Source/MQTTnet/Server/MqttClientSession.cs +++ b/Source/MQTTnet/Server/MqttClientSession.cs @@ -1,504 +1,72 @@ 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.Internal; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Serializer; +using MQTTnet.Server.Status; namespace MQTTnet.Server { - public class MqttClientSession : IMqttClientSession + public class MqttClientSession { - private readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); - - private readonly MqttRetainedMessagesManager _retainedMessagesManager; - private readonly MqttServerEventDispatcher _eventDispatcher; - private readonly MqttClientKeepAliveMonitor _keepAliveMonitor; - private readonly MqttClientPendingPacketsQueue _pendingPacketsQueue; - private readonly MqttClientSubscriptionsManager _subscriptionsManager; - private readonly MqttClientSessionsManager _sessionsManager; - private readonly IMqttNetChildLogger _logger; - private readonly IMqttServerOptions _options; - private CancellationTokenSource _cancellationTokenSource; - private MqttApplicationMessage _willMessage; - private bool _wasCleanDisconnect; - private Task _workerTask; - private IDisposable _cleanupHandle; + private readonly DateTime _createdTimestamp = DateTime.UtcNow; - private string _adapterEndpoint; - private MqttProtocolVersion? _adapterProtocolVersion; - - public MqttClientSession( - string clientId, - IMqttServerOptions options, - MqttClientSessionsManager sessionsManager, - MqttRetainedMessagesManager retainedMessagesManager, - MqttServerEventDispatcher eventDispatcher, - IMqttNetChildLogger logger) + public MqttClientSession(string clientId, MqttServerEventDispatcher eventDispatcher, IMqttServerOptions serverOptions, IMqttNetChildLogger logger) { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + + SubscriptionsManager = new MqttClientSubscriptionsManager(clientId, eventDispatcher, serverOptions); + ApplicationMessagesQueue = new MqttClientSessionApplicationMessagesQueue(serverOptions); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - - _options = options ?? throw new ArgumentNullException(nameof(options)); - _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); - _eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher)); - - ClientId = clientId; - _logger = logger.CreateChildLogger(nameof(MqttClientSession)); - - _keepAliveMonitor = new MqttClientKeepAliveMonitor(this, _logger); - _subscriptionsManager = new MqttClientSubscriptionsManager(clientId, _options, eventDispatcher); - _pendingPacketsQueue = new MqttClientPendingPacketsQueue(_options, this, _logger); } public string ClientId { get; } - public void FillStatus(MqttClientSessionStatus status) - { - status.ClientId = ClientId; - status.IsConnected = _cancellationTokenSource != null; - status.Endpoint = _adapterEndpoint; - status.ProtocolVersion = _adapterProtocolVersion; - status.PendingApplicationMessagesCount = _pendingPacketsQueue.Count; - status.LastPacketReceived = _keepAliveMonitor.LastPacketReceived; - status.LastNonKeepAlivePacketReceived = _keepAliveMonitor.LastNonKeepAlivePacketReceived; - } + public bool IsCleanSession { get; set; } = true; - public Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) - { - _workerTask = RunInternalAsync(connectPacket, adapter); - return _workerTask; - } + public MqttApplicationMessage WillMessage { get; set; } - public void Stop(MqttClientDisconnectType type) - { - Stop(type, false); - } + public MqttClientSubscriptionsManager SubscriptionsManager { get; } - public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); + public MqttClientSessionApplicationMessagesQueue ApplicationMessagesQueue { get; } - var checkSubscriptionsResult = _subscriptionsManager.CheckSubscriptions(publishPacket.Topic, publishPacket.QualityOfServiceLevel); + public void EnqueueApplicationMessage(MqttApplicationMessage applicationMessage, string senderClientId, bool isRetainedApplicationMessage) + { + var checkSubscriptionsResult = SubscriptionsManager.CheckSubscriptions(applicationMessage.Topic, applicationMessage.QualityOfServiceLevel); if (!checkSubscriptionsResult.IsSubscribed) { return; } - publishPacket = new MqttPublishPacket - { - Topic = publishPacket.Topic, - Payload = publishPacket.Payload, - QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel, - Retain = publishPacket.Retain, - Dup = false - }; + _logger.Verbose("Queued application message with topic '{0}' (ClientId: {1}).", applicationMessage.Topic, ClientId); - if (publishPacket.QualityOfServiceLevel > 0) - { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNewPacketIdentifier(); - } - - if (_options.ClientMessageQueueInterceptor != null) - { - var context = new MqttClientMessageQueueInterceptorContext( - senderClientSession?.ClientId, - ClientId, - publishPacket.ToApplicationMessage()); - - _options.ClientMessageQueueInterceptor?.Invoke(context); - - if (!context.AcceptEnqueue || context.ApplicationMessage == null) - { - return; - } - - publishPacket.Topic = context.ApplicationMessage.Topic; - publishPacket.Payload = context.ApplicationMessage.Payload; - publishPacket.QualityOfServiceLevel = context.ApplicationMessage.QualityOfServiceLevel; - } - - _pendingPacketsQueue.Enqueue(publishPacket); + ApplicationMessagesQueue.Enqueue(applicationMessage, senderClientId, checkSubscriptionsResult.QualityOfServiceLevel, isRetainedApplicationMessage); } - public Task SubscribeAsync(IList topicFilters) + public async Task SubscribeAsync(ICollection topicFilters, MqttRetainedMessagesManager retainedMessagesManager) { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + await SubscriptionsManager.SubscribeAsync(topicFilters).ConfigureAwait(false); - _subscriptionsManager.Subscribe(new MqttSubscribePacket + var matchingRetainedMessages = await retainedMessagesManager.GetSubscribedMessagesAsync(topicFilters).ConfigureAwait(false); + foreach (var matchingRetainedMessage in matchingRetainedMessages) { - TopicFilters = topicFilters - }); - - EnqueueSubscribedRetainedMessages(topicFilters); - return Task.FromResult(0); - } - - public Task UnsubscribeAsync(IList topicFilters) - { - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - _subscriptionsManager.Unsubscribe(new MqttUnsubscribePacket - { - TopicFilters = topicFilters - }); - - return Task.FromResult(0); - } - - public void ClearPendingApplicationMessages() - { - _pendingPacketsQueue.Clear(); - } - - public void Dispose() - { - _pendingPacketsQueue?.Dispose(); - - _cancellationTokenSource?.Cancel (); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - } - - private void Stop(MqttClientDisconnectType type, bool isInsideSession) - { - try - { - var cts = _cancellationTokenSource; - if (cts == null || cts.IsCancellationRequested) - { - return; - } - - _cancellationTokenSource?.Cancel(false); - - _wasCleanDisconnect = type == MqttClientDisconnectType.Clean; - - if (_willMessage != null && !_wasCleanDisconnect) - { - _sessionsManager.EnqueueApplicationMessage(this, _willMessage.ToPublishPacket()); - } - - _willMessage = null; - - if (!isInsideSession) - { - _workerTask?.GetAwaiter().GetResult(); - } - } - finally - { - _logger.Info("Client '{0}': Disconnected (clean={1}).", ClientId, _wasCleanDisconnect); - _eventDispatcher.OnClientDisconnected(ClientId, _wasCleanDisconnect); + EnqueueApplicationMessage(matchingRetainedMessage, null, true); } } - private async Task RunInternalAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) + public Task UnsubscribeAsync(IEnumerable topicFilters) { - if (connectPacket == null) throw new ArgumentNullException(nameof(connectPacket)); - if (adapter == null) throw new ArgumentNullException(nameof(adapter)); - - try - { - if (_cancellationTokenSource != null) - { - Stop(MqttClientDisconnectType.Clean, true); - } - - adapter.ReadingPacketStarted += OnAdapterReadingPacketStarted; - adapter.ReadingPacketCompleted += OnAdapterReadingPacketCompleted; - - _cancellationTokenSource = new CancellationTokenSource(); - - //workaround for https://github.com/dotnet/corefx/issues/24430 -#pragma warning disable 4014 - _cleanupHandle = _cancellationTokenSource.Token.Register(async () => - { - await TryDisconnectAdapterAsync(adapter).ConfigureAwait(false); - TryDisposeAdapter(adapter); - }); -#pragma warning restore 4014 - //end workaround - - _wasCleanDisconnect = false; - _willMessage = connectPacket.WillMessage; - - _pendingPacketsQueue.Start(adapter, _cancellationTokenSource.Token); - _keepAliveMonitor.Start(connectPacket.KeepAlivePeriod, _cancellationTokenSource.Token); - - _adapterEndpoint = adapter.Endpoint; - _adapterProtocolVersion = adapter.PacketSerializer.ProtocolVersion; - - while (!_cancellationTokenSource.IsCancellationRequested) - { - var packet = await adapter.ReceivePacketAsync(TimeSpan.Zero, _cancellationTokenSource.Token).ConfigureAwait(false); - if (packet != null) - { - _keepAliveMonitor.PacketReceived(packet); - ProcessReceivedPacket(adapter, packet, _cancellationTokenSource.Token); - } - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - if (exception is MqttCommunicationException) - { - if (exception is MqttCommunicationClosedGracefullyException) - { - _logger.Verbose("Client '{0}': Connection closed gracefully.", ClientId); - } - else - { - _logger.Warning(exception, "Client '{0}': Communication exception while receiving client packets.", ClientId); - } - } - else - { - _logger.Error(exception, "Client '{0}': Unhandled exception while receiving client packets.", ClientId); - } - - Stop(MqttClientDisconnectType.NotClean, true); - } - finally - { - _adapterEndpoint = null; - _adapterProtocolVersion = null; - - // Uncomment as soon as the workaround above is no longer needed. - //await TryDisconnectAdapterAsync(adapter).ConfigureAwait(false); - //TryDisposeAdapter(adapter); - - _cleanupHandle?.Dispose(); - _cleanupHandle = null; - - _cancellationTokenSource?.Cancel(false); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - } + return SubscriptionsManager.UnsubscribeAsync(topicFilters); } - private void TryDisposeAdapter(IMqttChannelAdapter adapter) + public void FillStatus(MqttSessionStatus status) { - if (adapter == null) - { - return; - } - - try - { - adapter.ReadingPacketStarted -= OnAdapterReadingPacketStarted; - adapter.ReadingPacketCompleted -= OnAdapterReadingPacketCompleted; - - adapter.Dispose(); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while disposing channel adapter."); - } - } - - private async Task TryDisconnectAdapterAsync(IMqttChannelAdapter adapter) - { - if (adapter == null) - { - return; - } - - try - { - await adapter.DisconnectAsync(_options.DefaultCommunicationTimeout, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while disconnecting channel adapter."); - } - } - - private void ProcessReceivedPacket(IMqttChannelAdapter adapter, MqttBasePacket packet, CancellationToken cancellationToken) - { - if (packet is MqttPublishPacket publishPacket) - { - HandleIncomingPublishPacket(adapter, publishPacket, cancellationToken); - return; - } - - if (packet is MqttPingReqPacket) - { - adapter.SendPacketAsync(new MqttPingRespPacket(), cancellationToken).GetAwaiter().GetResult(); - return; - } - - if (packet is MqttPubRelPacket pubRelPacket) - { - var responsePacket = new MqttPubCompPacket - { - PacketIdentifier = pubRelPacket.PacketIdentifier - }; - - adapter.SendPacketAsync(responsePacket, cancellationToken).GetAwaiter().GetResult(); - return; - } - - if (packet is MqttPubRecPacket pubRecPacket) - { - var responsePacket = new MqttPubRelPacket - { - PacketIdentifier = pubRecPacket.PacketIdentifier - }; - - adapter.SendPacketAsync(responsePacket, cancellationToken).GetAwaiter().GetResult(); - return; - } - - if (packet is MqttPubAckPacket || packet is MqttPubCompPacket) - { - return; - } - - if (packet is MqttSubscribePacket subscribePacket) - { - HandleIncomingSubscribePacket(adapter, subscribePacket, cancellationToken); - return; - } - - if (packet is MqttUnsubscribePacket unsubscribePacket) - { - HandleIncomingUnsubscribePacket(adapter, unsubscribePacket, cancellationToken); - return; - } - - if (packet is MqttDisconnectPacket) - { - Stop(MqttClientDisconnectType.Clean, true); - return; - } - - if (packet is MqttConnectPacket) - { - Stop(MqttClientDisconnectType.NotClean, true); - return; - } - - _logger.Warning(null, "Client '{0}': Received not supported packet ({1}). Closing connection.", ClientId, packet); - Stop(MqttClientDisconnectType.NotClean, true); - } - - private void EnqueueSubscribedRetainedMessages(ICollection topicFilters) - { - var retainedMessages = _retainedMessagesManager.GetSubscribedMessages(topicFilters); - foreach (var applicationMessage in retainedMessages) - { - var publishPacket = applicationMessage.ToPublishPacket(); - - // Set the retain flag to true according to [MQTT-3.3.1-8]. - publishPacket.Retain = true; - - EnqueueApplicationMessage(null, publishPacket); - } - } - - private void HandleIncomingSubscribePacket(IMqttChannelAdapter adapter, MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) - { - var subscribeResult = _subscriptionsManager.Subscribe(subscribePacket); - adapter.SendPacketAsync(subscribeResult.ResponsePacket, cancellationToken).GetAwaiter().GetResult(); - - if (subscribeResult.CloseConnection) - { - Stop(MqttClientDisconnectType.NotClean, true); - return; - } - - EnqueueSubscribedRetainedMessages(subscribePacket.TopicFilters); - } - - private void HandleIncomingUnsubscribePacket(IMqttChannelAdapter adapter, MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) - { - var unsubscribeResult = _subscriptionsManager.Unsubscribe(unsubscribePacket); - adapter.SendPacketAsync(unsubscribeResult, cancellationToken).GetAwaiter().GetResult(); - } - - private void HandleIncomingPublishPacket(IMqttChannelAdapter adapter, MqttPublishPacket publishPacket, CancellationToken cancellationToken) - { - switch (publishPacket.QualityOfServiceLevel) - { - case MqttQualityOfServiceLevel.AtMostOnce: - { - HandleIncomingPublishPacketWithQoS0(publishPacket); - break; - } - case MqttQualityOfServiceLevel.AtLeastOnce: - { - HandleIncomingPublishPacketWithQoS1(adapter, publishPacket, cancellationToken); - break; - } - case MqttQualityOfServiceLevel.ExactlyOnce: - { - HandleIncomingPublishPacketWithQoS2(adapter, publishPacket, cancellationToken); - break; - } - default: - { - throw new MqttCommunicationException("Received a not supported QoS level."); - } - } - } - - private void HandleIncomingPublishPacketWithQoS0(MqttPublishPacket publishPacket) - { - _sessionsManager.EnqueueApplicationMessage(this, publishPacket); - } - - private void HandleIncomingPublishPacketWithQoS1( - IMqttChannelAdapter adapter, - MqttPublishPacket publishPacket, - CancellationToken cancellationToken) - { - _sessionsManager.EnqueueApplicationMessage(this, publishPacket); - - var response = new MqttPubAckPacket - { - PacketIdentifier = publishPacket.PacketIdentifier - }; - - adapter.SendPacketAsync(response, cancellationToken).GetAwaiter().GetResult(); - } - - private void HandleIncomingPublishPacketWithQoS2( - IMqttChannelAdapter adapter, - MqttPublishPacket publishPacket, - CancellationToken cancellationToken) - { - // QoS 2 is implement as method "B" (4.3.3 QoS 2: Exactly once delivery) - _sessionsManager.EnqueueApplicationMessage(this, publishPacket); - - var response = new MqttPubRecPacket - { - PacketIdentifier = publishPacket.PacketIdentifier - }; - - adapter.SendPacketAsync(response, cancellationToken).GetAwaiter().GetResult(); - } - - private void OnAdapterReadingPacketCompleted(object sender, EventArgs e) - { - _keepAliveMonitor?.Resume(); - } - - private void OnAdapterReadingPacketStarted(object sender, EventArgs e) - { - _keepAliveMonitor?.Pause(); + status.ClientId = ClientId; + status.CreatedTimestamp = _createdTimestamp; + status.PendingApplicationMessagesCount = ApplicationMessagesQueue.Count; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttClientSessionApplicationMessagesQueue.cs b/Source/MQTTnet/Server/MqttClientSessionApplicationMessagesQueue.cs new file mode 100644 index 00000000..901ac757 --- /dev/null +++ b/Source/MQTTnet/Server/MqttClientSessionApplicationMessagesQueue.cs @@ -0,0 +1,79 @@ +using MQTTnet.Internal; +using MQTTnet.Protocol; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttClientSessionApplicationMessagesQueue : IDisposable + { + private readonly AsyncQueue _messageQueue = new AsyncQueue(); + + private readonly IMqttServerOptions _options; + + public MqttClientSessionApplicationMessagesQueue(IMqttServerOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public int Count => _messageQueue.Count; + + public void Enqueue(MqttApplicationMessage applicationMessage, string senderClientId, MqttQualityOfServiceLevel qualityOfServiceLevel, bool isRetainedMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + Enqueue(new MqttQueuedApplicationMessage + { + ApplicationMessage = applicationMessage, + SenderClientId = senderClientId, + QualityOfServiceLevel = qualityOfServiceLevel, + IsRetainedMessage = isRetainedMessage + }); + } + + public void Clear() + { + _messageQueue.Clear(); + } + + public async Task TakeAsync(CancellationToken cancellationToken) + { + var dequeueResult = await _messageQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false); + if (!dequeueResult.IsSuccess) + { + return null; + } + + return dequeueResult.Item; + } + + public void Enqueue(MqttQueuedApplicationMessage queuedApplicationMessage) + { + if (queuedApplicationMessage == null) throw new ArgumentNullException(nameof(queuedApplicationMessage)); + + lock (_messageQueue) + { + if (_messageQueue.Count >= _options.MaxPendingMessagesPerClient) + { + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + { + return; + } + + if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + { + _messageQueue.TryDequeue(); + } + } + + _messageQueue.Enqueue(queuedApplicationMessage); + } + } + + public void Dispose() + { + _messageQueue.Dispose(); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientSessionStatus.cs b/Source/MQTTnet/Server/MqttClientSessionStatus.cs deleted file mode 100644 index 4d83357e..00000000 --- a/Source/MQTTnet/Server/MqttClientSessionStatus.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Serializer; - -namespace MQTTnet.Server -{ - public class MqttClientSessionStatus : IMqttClientSessionStatus - { - private readonly MqttClientSessionsManager _sessionsManager; - private readonly MqttClientSession _session; - - public MqttClientSessionStatus(MqttClientSessionsManager sessionsManager, MqttClientSession session) - { - _sessionsManager = sessionsManager; - _session = session; - } - - public string ClientId { get; set; } - public string Endpoint { get; set; } - public bool IsConnected { get; set; } - public MqttProtocolVersion? ProtocolVersion { get; set; } - public TimeSpan LastPacketReceived { get; set; } - public TimeSpan LastNonKeepAlivePacketReceived { get; set; } - public int PendingApplicationMessagesCount { get; set; } - - public Task DisconnectAsync() - { - _session.Stop(MqttClientDisconnectType.NotClean); - return Task.FromResult(0); - } - - public Task DeleteSessionAsync() - { - try - { - _session.Stop(MqttClientDisconnectType.NotClean); - } - finally - { - _sessionsManager.DeleteSession(ClientId); - } - - return Task.FromResult(0); - } - - public Task ClearPendingApplicationMessagesAsync() - { - _session.ClearPendingApplicationMessages(); - - return Task.FromResult(0); - } - } -} diff --git a/Source/MQTTnet/Server/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/MqttClientSessionsManager.cs index 7b7e3480..fdc62b01 100644 --- a/Source/MQTTnet/Server/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/MqttClientSessionsManager.cs @@ -1,27 +1,25 @@ using System; 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.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; +using MQTTnet.Server.Status; namespace MQTTnet.Server { public class MqttClientSessionsManager : IDisposable { - private readonly BlockingCollection _messageQueue = new BlockingCollection(); - - /// - /// manual locking dictionaries is faster than using concurrent dictionary - /// - private readonly Dictionary _sessions = new Dictionary(); + private readonly AsyncQueue _messageQueue = new AsyncQueue(); + private readonly SemaphoreSlim _createConnectionGate = new SemaphoreSlim(1, 1); + private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); + private readonly CancellationToken _cancellationToken; private readonly MqttServerEventDispatcher _eventDispatcher; @@ -30,8 +28,8 @@ namespace MQTTnet.Server private readonly IMqttNetChildLogger _logger; public MqttClientSessionsManager( - IMqttServerOptions options, - MqttRetainedMessagesManager retainedMessagesManager, + IMqttServerOptions options, + MqttRetainedMessagesManager retainedMessagesManager, CancellationToken cancellationToken, MqttServerEventDispatcher eventDispatcher, IMqttNetChildLogger logger) @@ -48,86 +46,98 @@ namespace MQTTnet.Server public void Start() { - Task.Factory.StartNew(() => TryProcessQueuedApplicationMessages(_cancellationToken), _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + Task.Run(() => TryProcessQueuedApplicationMessagesAsync(_cancellationToken), _cancellationToken); } - public void Stop() + public async Task StopAsync() { - lock (_sessions) + foreach (var connection in _connections.Values) { - foreach (var session in _sessions) - { - session.Value.Stop(MqttClientDisconnectType.NotClean); - } - - _sessions.Clear(); + await connection.StopAsync().ConfigureAwait(false); } } - public Task StartSession(IMqttChannelAdapter clientAdapter) + public Task HandleClientAsync(IMqttChannelAdapter clientAdapter) { - return Task.Run(() => RunSessionAsync(clientAdapter, _cancellationToken), _cancellationToken); + return HandleClientAsync(clientAdapter, _cancellationToken); } - public IList GetClientStatus() + public Task> GetClientStatusAsync() { - var result = new List(); + var result = new List(); - foreach (var session in GetSessions()) + foreach (var connection in _connections.Values) { - var status = new MqttClientSessionStatus(this, session); - session.FillStatus(status); + var clientStatus = new MqttClientStatus(connection); + connection.FillStatus(clientStatus); + + var sessionStatus = new MqttSessionStatus(connection.Session, this); + connection.Session.FillStatus(sessionStatus); + clientStatus.Session = sessionStatus; - result.Add(status); + result.Add(clientStatus); } - return result; + return Task.FromResult((IList)result); } - public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) + public Task> GetSessionStatusAsync() { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); + var result = new List(); - _messageQueue.Add(new MqttEnqueuedApplicationMessage(senderClientSession, publishPacket), _cancellationToken); + foreach (var session in _sessions.Values) + { + var sessionStatus = new MqttSessionStatus(session, this); + session.FillStatus(sessionStatus); + + result.Add(sessionStatus); + } + + return Task.FromResult((IList)result); } - public Task SubscribeAsync(string clientId, IList topicFilters) + public void DispatchApplicationMessage(MqttApplicationMessage applicationMessage, MqttClientConnection sender) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + _messageQueue.Enqueue(new MqttEnqueuedApplicationMessage(applicationMessage, sender)); + } + + public Task SubscribeAsync(string clientId, ICollection topicFilters) { if (clientId == null) throw new ArgumentNullException(nameof(clientId)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - lock (_sessions) + if (!_sessions.TryGetValue(clientId, out var session)) { - if (!_sessions.TryGetValue(clientId, out var session)) - { - throw new InvalidOperationException($"Client session '{clientId}' is unknown."); - } - - return session.SubscribeAsync(topicFilters); + throw new InvalidOperationException($"Client session '{clientId}' is unknown."); } + + return session.SubscribeAsync(topicFilters, _retainedMessagesManager); } - public Task UnsubscribeAsync(string clientId, IList topicFilters) + public Task UnsubscribeAsync(string clientId, IEnumerable topicFilters) { if (clientId == null) throw new ArgumentNullException(nameof(clientId)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - lock (_sessions) + if (!_sessions.TryGetValue(clientId, out var session)) { - if (!_sessions.TryGetValue(clientId, out var session)) - { - throw new InvalidOperationException($"Client session '{clientId}' is unknown."); - } - - return session.UnsubscribeAsync(topicFilters); + throw new InvalidOperationException($"Client session '{clientId}' is unknown."); } + + return session.UnsubscribeAsync(topicFilters); } - public void DeleteSession(string clientId) + public async Task DeleteSessionAsync(string clientId) { - lock (_sessions) + if (_connections.TryGetValue(clientId, out var connection)) + { + await connection.StopAsync().ConfigureAwait(false); + } + + if (_sessions.TryRemove(clientId, out var session)) { - _sessions.Remove(clientId); } _logger.Verbose("Session for client '{0}' deleted.", clientId); @@ -138,13 +148,13 @@ namespace MQTTnet.Server _messageQueue?.Dispose(); } - private void TryProcessQueuedApplicationMessages(CancellationToken cancellationToken) + private async Task TryProcessQueuedApplicationMessagesAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { - TryProcessNextQueuedApplicationMessage(cancellationToken); + await TryProcessNextQueuedApplicationMessageAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -156,20 +166,30 @@ namespace MQTTnet.Server } } - private void TryProcessNextQueuedApplicationMessage(CancellationToken cancellationToken) + private async Task TryProcessNextQueuedApplicationMessageAsync(CancellationToken cancellationToken) { try { - var enqueuedApplicationMessage = _messageQueue.Take(cancellationToken); - var sender = enqueuedApplicationMessage.Sender; - var applicationMessage = enqueuedApplicationMessage.PublishPacket.ToApplicationMessage(); + if (cancellationToken.IsCancellationRequested) + { + return; + } - var interceptorContext = InterceptApplicationMessage(sender, applicationMessage); + var dequeueResult = await _messageQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false); + var queuedApplicationMessage = dequeueResult.Item; + + var sender = queuedApplicationMessage.Sender; + var applicationMessage = queuedApplicationMessage.ApplicationMessage; + + var interceptorContext = await InterceptApplicationMessageAsync(sender, applicationMessage).ConfigureAwait(false); if (interceptorContext != null) { if (interceptorContext.CloseConnection) { - enqueuedApplicationMessage.Sender.Stop(MqttClientDisconnectType.NotClean); + if (sender != null) + { + await sender.StopAsync().ConfigureAwait(false); + } } if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish) @@ -180,21 +200,19 @@ namespace MQTTnet.Server applicationMessage = interceptorContext.ApplicationMessage; } - _eventDispatcher.OnApplicationMessageReceived(sender?.ClientId, applicationMessage); + await _eventDispatcher.HandleApplicationMessageReceivedAsync(sender?.ClientId, applicationMessage).ConfigureAwait(false); if (applicationMessage.Retain) { - _retainedMessagesManager.HandleMessageAsync(sender?.ClientId, applicationMessage).GetAwaiter().GetResult(); + await _retainedMessagesManager.HandleMessageAsync(sender?.ClientId, applicationMessage).ConfigureAwait(false); } - foreach (var clientSession in GetSessions()) + foreach (var clientSession in _sessions.Values) { - var publishPacket = applicationMessage.ToPublishPacket(); - - // Set the retain flag to true according to [MQTT-3.3.1-9]. - publishPacket.Retain = false; - - clientSession.EnqueueApplicationMessage(enqueuedApplicationMessage.Sender, publishPacket); + clientSession.EnqueueApplicationMessage( + applicationMessage, + sender?.ClientId, + false); } } catch (OperationCanceledException) @@ -206,63 +224,47 @@ namespace MQTTnet.Server } } - private List GetSessions() - { - lock (_sessions) - { - return _sessions.Values.ToList(); - } - } - - private async Task RunSessionAsync(IMqttChannelAdapter clientAdapter, CancellationToken cancellationToken) + private async Task HandleClientAsync(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) { + var disconnectType = MqttClientDisconnectType.NotClean; var clientId = string.Empty; - + try { - var firstPacket = await clientAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken).ConfigureAwait(false); - if (firstPacket == null) - { - return; - } - + var firstPacket = await channelAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken).ConfigureAwait(false); if (!(firstPacket is MqttConnectPacket connectPacket)) { - throw new MqttProtocolViolationException("The first packet from a client must be a 'CONNECT' packet [MQTT-3.1.0-1]."); + _logger.Warning(null, "The first packet from client '{0}' was no 'CONNECT' packet [MQTT-3.1.0-1].", channelAdapter.Endpoint); + return; } clientId = connectPacket.ClientId; - // Switch to the required protocol version before sending any response. - clientAdapter.PacketSerializer.ProtocolVersion = connectPacket.ProtocolVersion; + var validatorContext = await ValidateConnectionAsync(connectPacket, channelAdapter).ConfigureAwait(false); - var connectReturnCode = ValidateConnection(connectPacket, clientAdapter); - if (connectReturnCode != MqttConnectReturnCode.ConnectionAccepted) + if (validatorContext.ReturnCode != MqttConnectReturnCode.ConnectionAccepted) { - await clientAdapter.SendPacketAsync( + // TODO: Move to channel adapter data converter. + + // Send failure response here without preparing a session. The result for a successful connect + // will be sent from the session itself. + await channelAdapter.SendPacketAsync( new MqttConnAckPacket { - ConnectReturnCode = connectReturnCode + ReturnCode = validatorContext.ReturnCode, + ReasonCode = MqttConnectReasonCode.NotAuthorized }, + _options.DefaultCommunicationTimeout, cancellationToken).ConfigureAwait(false); return; } - var result = PrepareClientSession(connectPacket); + var connection = await CreateConnectionAsync(channelAdapter, connectPacket).ConfigureAwait(false); - await clientAdapter.SendPacketAsync( - new MqttConnAckPacket - { - ConnectReturnCode = connectReturnCode, - IsSessionPresent = result.IsExistingSession - }, - cancellationToken).ConfigureAwait(false); - - _logger.Info("Client '{0}': Connected.", clientId); - _eventDispatcher.OnClientConnected(clientId); - - await result.Session.RunAsync(connectPacket, clientAdapter).ConfigureAwait(false); + await _eventDispatcher.HandleClientConnectedAsync(clientId).ConfigureAwait(false); + + disconnectType = await connection.RunAsync().ConfigureAwait(false); } catch (OperationCanceledException) { @@ -273,47 +275,61 @@ namespace MQTTnet.Server } finally { + _connections.TryRemove(clientId, out _); + if (!_options.EnablePersistentSessions) { - DeleteSession(clientId); + await DeleteSessionAsync(clientId).ConfigureAwait(false); } + + await TryCleanupChannelAsync(channelAdapter).ConfigureAwait(false); + + await _eventDispatcher.HandleClientDisconnectedAsync(clientId, disconnectType).ConfigureAwait(false); } } - private MqttConnectReturnCode ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter) + private async Task ValidateConnectionAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter) { - if (_options.ConnectionValidator == null) - { - return MqttConnectReturnCode.ConnectionAccepted; - } - var context = new MqttConnectionValidatorContext( connectPacket.ClientId, connectPacket.Username, connectPacket.Password, connectPacket.WillMessage, - clientAdapter.Endpoint); + clientAdapter.Endpoint, + clientAdapter.IsSecureConnection); - _options.ConnectionValidator(context); - return context.ReturnCode; + var connectionValidator = _options.ConnectionValidator; + + if (connectionValidator == null) + { + context.ReturnCode = MqttConnectReturnCode.ConnectionAccepted; + return context; + } + + await connectionValidator.ValidateConnectionAsync(context).ConfigureAwait(false); + return context; } - private PrepareClientSessionResult PrepareClientSession(MqttConnectPacket connectPacket) + private async Task CreateConnectionAsync(IMqttChannelAdapter channelAdapter, MqttConnectPacket connectPacket) { - lock (_sessions) + await _createConnectionGate.WaitAsync(_cancellationToken).ConfigureAwait(false); + try { - var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession); + var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var session); + + var isConnectionPresent = _connections.TryGetValue(connectPacket.ClientId, out var existingConnection); + if (isConnectionPresent) + { + await existingConnection.StopAsync().ConfigureAwait(false); + } + if (isSessionPresent) { if (connectPacket.CleanSession) { - _sessions.Remove(connectPacket.ClientId); - - clientSession.Stop(MqttClientDisconnectType.Clean); - clientSession.Dispose(); - clientSession = null; - - _logger.Verbose("Stopped existing session of client '{0}'.", connectPacket.ClientId); + session = null; + + _logger.Verbose("Deleting existing session of client '{0}'.", connectPacket.ClientId); } else { @@ -321,22 +337,26 @@ namespace MQTTnet.Server } } - var isExistingSession = true; - if (clientSession == null) + if (session == null) { - isExistingSession = false; - - clientSession = new MqttClientSession(connectPacket.ClientId, _options, this, _retainedMessagesManager, _eventDispatcher, _logger); - _sessions[connectPacket.ClientId] = clientSession; - + session = new MqttClientSession(connectPacket.ClientId, _eventDispatcher, _options, _logger); _logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId); } - return new PrepareClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; + var connection = new MqttClientConnection(connectPacket, channelAdapter, session, _options, this, _retainedMessagesManager, _logger); + + _connections[connection.ClientId] = connection; + _sessions[session.ClientId] = session; + + return connection; + } + finally + { + _createConnectionGate.Release(); } } - private MqttApplicationMessageInterceptorContext InterceptApplicationMessage(MqttClientSession sender, MqttApplicationMessage applicationMessage) + private async Task InterceptApplicationMessageAsync(MqttClientConnection sender, MqttApplicationMessage applicationMessage) { var interceptor = _options.ApplicationMessageInterceptor; if (interceptor == null) @@ -344,9 +364,27 @@ namespace MQTTnet.Server return null; } - var interceptorContext = new MqttApplicationMessageInterceptorContext(sender?.ClientId, applicationMessage); - interceptor(interceptorContext); + var senderClientId = sender?.ClientId; + if (sender == null) + { + senderClientId = _options.ClientId; + } + + var interceptorContext = new MqttApplicationMessageInterceptorContext(senderClientId, applicationMessage); + await interceptor.InterceptApplicationMessagePublishAsync(interceptorContext).ConfigureAwait(false); return interceptorContext; } + + private async Task TryCleanupChannelAsync(IMqttChannelAdapter channelAdapter) + { + try + { + await channelAdapter.DisconnectAsync(_options.DefaultCommunicationTimeout, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while disconnecting client channel."); + } + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs index 38fad1f4..904474f5 100644 --- a/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/MqttClientSubscriptionsManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -13,14 +14,14 @@ namespace MQTTnet.Server private readonly MqttServerEventDispatcher _eventDispatcher; private readonly string _clientId; - public MqttClientSubscriptionsManager(string clientId, IMqttServerOptions options, MqttServerEventDispatcher eventDispatcher) + public MqttClientSubscriptionsManager(string clientId, MqttServerEventDispatcher eventDispatcher, IMqttServerOptions options) { _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); _options = options ?? throw new ArgumentNullException(nameof(options)); _eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher)); } - public MqttClientSubscribeResult Subscribe(MqttSubscribePacket subscribePacket) + public async Task SubscribeAsync(MqttSubscribePacket subscribePacket) { if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); @@ -36,14 +37,16 @@ namespace MQTTnet.Server foreach (var topicFilter in subscribePacket.TopicFilters) { - var interceptorContext = InterceptSubscribe(topicFilter); + var interceptorContext = await InterceptSubscribeAsync(topicFilter).ConfigureAwait(false); if (!interceptorContext.AcceptSubscription) { - result.ResponsePacket.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); + result.ResponsePacket.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); + result.ResponsePacket.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError); } else { - result.ResponsePacket.SubscribeReturnCodes.Add(ConvertToMaximumQoS(topicFilter.QualityOfServiceLevel)); + result.ResponsePacket.ReturnCodes.Add(ConvertToSubscribeReturnCode(topicFilter.QualityOfServiceLevel)); + result.ResponsePacket.ReasonCodes.Add(ConvertToSubscribeReasonCode(topicFilter.QualityOfServiceLevel)); } if (interceptorContext.CloseConnection) @@ -58,31 +61,80 @@ namespace MQTTnet.Server _subscriptions[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; } - _eventDispatcher.OnClientSubscribedTopic(_clientId, topicFilter); + await _eventDispatcher.HandleClientSubscribedTopicAsync(_clientId, topicFilter).ConfigureAwait(false); } } return result; } - public MqttUnsubAckPacket Unsubscribe(MqttUnsubscribePacket unsubscribePacket) + public async Task SubscribeAsync(IEnumerable topicFilters) + { + foreach (var topicFilter in topicFilters) + { + var interceptorContext = await InterceptSubscribeAsync(topicFilter).ConfigureAwait(false); + if (!interceptorContext.AcceptSubscription) + { + continue; + } + + if (interceptorContext.AcceptSubscription) + { + lock (_subscriptions) + { + _subscriptions[topicFilter.Topic] = topicFilter.QualityOfServiceLevel; + } + + await _eventDispatcher.HandleClientSubscribedTopicAsync(_clientId, topicFilter).ConfigureAwait(false); + } + } + } + + public async Task UnsubscribeAsync(MqttUnsubscribePacket unsubscribePacket) { if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + var unsubAckPacket = new MqttUnsubAckPacket + { + PacketIdentifier = unsubscribePacket.PacketIdentifier + }; + lock (_subscriptions) { foreach (var topicFilter in unsubscribePacket.TopicFilters) { - _subscriptions.Remove(topicFilter); - - _eventDispatcher.OnClientUnsubscribedTopic(_clientId, topicFilter); + if (_subscriptions.Remove(topicFilter)) + { + unsubAckPacket.ReasonCodes.Add(MqttUnsubscribeReasonCode.Success); + } + else + { + unsubAckPacket.ReasonCodes.Add(MqttUnsubscribeReasonCode.NoSubscriptionExisted); + } } } - return new MqttUnsubAckPacket + foreach (var topicFilter in unsubscribePacket.TopicFilters) { - PacketIdentifier = unsubscribePacket.PacketIdentifier - }; + await _eventDispatcher.HandleClientUnsubscribedTopicAsync(_clientId, topicFilter).ConfigureAwait(false); + } + + return unsubAckPacket; + } + + public Task UnsubscribeAsync(IEnumerable topicFilters) + { + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + lock (_subscriptions) + { + foreach (var topicFilter in topicFilters) + { + _subscriptions.Remove(topicFilter); + } + } + + return Task.FromResult(0); } public CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel qosLevel) @@ -113,7 +165,7 @@ namespace MQTTnet.Server return CreateSubscriptionResult(qosLevel, qosLevels); } - private static MqttSubscribeReturnCode ConvertToMaximumQoS(MqttQualityOfServiceLevel qualityOfServiceLevel) + private static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel) { switch (qualityOfServiceLevel) { @@ -124,11 +176,26 @@ namespace MQTTnet.Server } } - private MqttSubscriptionInterceptorContext InterceptSubscribe(TopicFilter topicFilter) + private static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel) { - var interceptorContext = new MqttSubscriptionInterceptorContext(_clientId, topicFilter); - _options.SubscriptionInterceptor?.Invoke(interceptorContext); - return interceptorContext; + switch (qualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: return MqttSubscribeReasonCode.GrantedQoS0; + case MqttQualityOfServiceLevel.AtLeastOnce: return MqttSubscribeReasonCode.GrantedQoS1; + case MqttQualityOfServiceLevel.ExactlyOnce: return MqttSubscribeReasonCode.GrantedQoS2; + default: return MqttSubscribeReasonCode.UnspecifiedError; + } + } + + private async Task InterceptSubscribeAsync(TopicFilter topicFilter) + { + var context = new MqttSubscriptionInterceptorContext(_clientId, topicFilter); + if (_options.SubscriptionInterceptor != null) + { + await _options.SubscriptionInterceptor.InterceptSubscriptionAsync(context).ConfigureAwait(false); + } + + return context; } private static CheckSubscriptionsResult CreateSubscriptionResult(MqttQualityOfServiceLevel qosLevel, HashSet subscribedQoSLevels) diff --git a/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs b/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs index 5857862b..b06eb844 100644 --- a/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs +++ b/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs @@ -4,13 +4,14 @@ namespace MQTTnet.Server { public class MqttConnectionValidatorContext { - public MqttConnectionValidatorContext(string clientId, string username, string password, MqttApplicationMessage willMessage, string endpoint) + public MqttConnectionValidatorContext(string clientId, string username, string password, MqttApplicationMessage willMessage, string endpoint, bool isSecureConnection) { ClientId = clientId; Username = username; Password = password; WillMessage = willMessage; Endpoint = endpoint; + IsSecureConnection = isSecureConnection; } public string ClientId { get; } @@ -23,6 +24,8 @@ namespace MQTTnet.Server public string Endpoint { get; } + public bool IsSecureConnection { get; } + public MqttConnectReturnCode ReturnCode { get; set; } = MqttConnectReturnCode.ConnectionAccepted; } } diff --git a/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs b/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs index 37591d89..47112692 100644 --- a/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs +++ b/Source/MQTTnet/Server/MqttEnqueuedApplicationMessage.cs @@ -1,17 +1,15 @@ -using MQTTnet.Packets; - -namespace MQTTnet.Server +namespace MQTTnet.Server { public class MqttEnqueuedApplicationMessage { - public MqttEnqueuedApplicationMessage(MqttClientSession sender, MqttPublishPacket publishPacket) + public MqttEnqueuedApplicationMessage(MqttApplicationMessage applicationMessage, MqttClientConnection sender) { Sender = sender; - PublishPacket = publishPacket; + ApplicationMessage = applicationMessage; } - public MqttClientSession Sender { get; } + public MqttClientConnection Sender { get; } - public MqttPublishPacket PublishPacket { get; } + public MqttApplicationMessage ApplicationMessage { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs b/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs new file mode 100644 index 00000000..5a3300ad --- /dev/null +++ b/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs @@ -0,0 +1,17 @@ +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public class MqttQueuedApplicationMessage + { + public MqttApplicationMessage ApplicationMessage { get; set; } + + public string SenderClientId { get; set; } + + public bool IsRetainedMessage { get; set; } + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + + public bool IsDuplicate { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs index bf5de984..2e6af16a 100644 --- a/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet/Server/MqttRetainedMessagesManager.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MQTTnet.Diagnostics; +using MQTTnet.Internal; namespace MQTTnet.Server { public class MqttRetainedMessagesManager { + private readonly byte[] _emptyArray = new byte[0]; + private readonly AsyncLock _messagesLock = new AsyncLock(); private readonly Dictionary _messages = new Dictionary(); private readonly IMqttNetChildLogger _logger; @@ -30,13 +33,16 @@ namespace MQTTnet.Server try { var retainedMessages = await _options.Storage.LoadRetainedMessagesAsync().ConfigureAwait(false); - - lock (_messages) + if (retainedMessages?.Any() == true) { - _messages.Clear(); - foreach (var retainedMessage in retainedMessages) + using (await _messagesLock.WaitAsync().ConfigureAwait(false)) { - _messages[retainedMessage.Topic] = retainedMessage; + _messages.Clear(); + + foreach (var retainedMessage in retainedMessages) + { + _messages[retainedMessage.Topic] = retainedMessage; + } } } } @@ -52,12 +58,12 @@ namespace MQTTnet.Server try { - List messagesForSave = null; - lock (_messages) + using (await _messagesLock.WaitAsync().ConfigureAwait(false)) { var saveIsRequired = false; + var hasPayload = applicationMessage.Payload != null && applicationMessage.Payload.Length > 0; - if (applicationMessage.Payload?.Length == 0) + if (!hasPayload) { saveIsRequired = _messages.Remove(applicationMessage.Topic); _logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); @@ -71,7 +77,7 @@ namespace MQTTnet.Server } else { - if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? new byte[0])) + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? _emptyArray)) { _messages[applicationMessage.Topic] = applicationMessage; saveIsRequired = true; @@ -83,20 +89,13 @@ namespace MQTTnet.Server if (saveIsRequired) { - messagesForSave = new List(_messages.Values); + if (_options.Storage != null) + { + var messagesForSave = new List(_messages.Values); + await _options.Storage.SaveRetainedMessagesAsync(messagesForSave).ConfigureAwait(false); + } } } - - if (messagesForSave == null) - { - _logger.Verbose("Skipped saving retained messages because no changes were detected."); - return; - } - - if (_options.Storage != null) - { - await _options.Storage.SaveRetainedMessagesAsync(messagesForSave).ConfigureAwait(false); - } } catch (Exception exception) { @@ -104,51 +103,54 @@ namespace MQTTnet.Server } } - public IList GetSubscribedMessages(ICollection topicFilters) + public async Task> GetSubscribedMessagesAsync(ICollection topicFilters) { - var retainedMessages = new List(); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - lock (_messages) + var matchingRetainedMessages = new List(); + + List retainedMessages; + using (await _messagesLock.WaitAsync().ConfigureAwait(false)) { - foreach (var retainedMessage in _messages.Values) - { - foreach (var topicFilter in topicFilters) - { - if (!MqttTopicFilterComparer.IsMatch(retainedMessage.Topic, topicFilter.Topic)) - { - continue; - } - - retainedMessages.Add(retainedMessage); - break; - } - } + retainedMessages = _messages.Values.ToList(); } - return retainedMessages; + foreach (var retainedMessage in retainedMessages) + { + foreach (var topicFilter in topicFilters) + { + if (!MqttTopicFilterComparer.IsMatch(retainedMessage.Topic, topicFilter.Topic)) + { + continue; + } + + matchingRetainedMessages.Add(retainedMessage); + break; + } + } + + return matchingRetainedMessages; } - public IList GetMessages() + public async Task> GetMessagesAsync() { - lock (_messages) + using (await _messagesLock.WaitAsync().ConfigureAwait(false)) { return _messages.Values.ToList(); } } - public Task ClearMessagesAsync() + public async Task ClearMessagesAsync() { - lock (_messages) + using (await _messagesLock.WaitAsync().ConfigureAwait(false)) { _messages.Clear(); - } - if (_options.Storage != null) - { - return _options.Storage.SaveRetainedMessagesAsync(new List()); + if (_options.Storage != null) + { + await _options.Storage.SaveRetainedMessagesAsync(new List()).ConfigureAwait(false); + } } - - return Task.FromResult((object)null); } } } diff --git a/Source/MQTTnet/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs index cb3e84e7..b2c32f45 100644 --- a/Source/MQTTnet/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -4,8 +4,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; using MQTTnet.Diagnostics; -using MQTTnet.Internal; +using MQTTnet.Protocol; +using MQTTnet.Server.Status; namespace MQTTnet.Server { @@ -26,42 +29,60 @@ namespace MQTTnet.Server if (logger == null) throw new ArgumentNullException(nameof(logger)); _logger = logger.CreateChildLogger(nameof(MqttServer)); - - _eventDispatcher.ClientConnected += (s, e) => ClientConnected?.Invoke(s, e); - _eventDispatcher.ClientDisconnected += (s, e) => ClientDisconnected?.Invoke(s, e); - _eventDispatcher.ClientSubscribedTopic += (s, e) => ClientSubscribedTopic?.Invoke(s, e); - _eventDispatcher.ClientUnsubscribedTopic += (s, e) => ClientUnsubscribedTopic?.Invoke(s, e); - _eventDispatcher.ApplicationMessageReceived += (s, e) => ApplicationMessageReceived?.Invoke(s, e); } - public event EventHandler Started; - public event EventHandler Stopped; + public IMqttServerStartedHandler StartedHandler { get; set; } - public event EventHandler ClientConnected; - public event EventHandler ClientDisconnected; - public event EventHandler ClientSubscribedTopic; - public event EventHandler ClientUnsubscribedTopic; + public IMqttServerStoppedHandler StoppedHandler { get; set; } - public event EventHandler ApplicationMessageReceived; + public IMqttServerClientConnectedHandler ClientConnectedHandler + { + get => _eventDispatcher.ClientConnectedHandler; + set => _eventDispatcher.ClientConnectedHandler = value; + } + + public IMqttServerClientDisconnectedHandler ClientDisconnectedHandler + { + get => _eventDispatcher.ClientDisconnectedHandler; + set => _eventDispatcher.ClientDisconnectedHandler = value; + } + + public IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler + { + get => _eventDispatcher.ClientSubscribedTopicHandler; + set => _eventDispatcher.ClientSubscribedTopicHandler = value; + } + + public IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler + { + get => _eventDispatcher.ClientUnsubscribedTopicHandler; + set => _eventDispatcher.ClientUnsubscribedTopicHandler = value; + } + + public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler + { + get => _eventDispatcher.ApplicationMessageReceivedHandler; + set => _eventDispatcher.ApplicationMessageReceivedHandler = value; + } public IMqttServerOptions Options { get; private set; } - public Task> GetClientSessionsStatusAsync() + public Task> GetClientStatusAsync() { - return Task.FromResult(_clientSessionsManager.GetClientStatus()); + return _clientSessionsManager.GetClientStatusAsync(); } - public IList GetClientSessionsStatus() + public Task> GetSessionStatusAsync() { - return _clientSessionsManager.GetClientStatus(); + return _clientSessionsManager.GetSessionStatusAsync(); } - public IList GetRetainedMessages() + public Task> GetRetainedApplicationMessagesAsync() { - return _retainedMessagesManager.GetMessages(); + return _retainedMessagesManager.GetMessagesAsync(); } - public Task SubscribeAsync(string clientId, IList topicFilters) + public Task SubscribeAsync(string clientId, ICollection topicFilters) { if (clientId == null) throw new ArgumentNullException(nameof(clientId)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); @@ -69,7 +90,7 @@ namespace MQTTnet.Server return _clientSessionsManager.SubscribeAsync(clientId, topicFilters); } - public Task UnsubscribeAsync(string clientId, IList topicFilters) + public Task UnsubscribeAsync(string clientId, ICollection topicFilters) { if (clientId == null) throw new ArgumentNullException(nameof(clientId)); if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); @@ -77,15 +98,17 @@ namespace MQTTnet.Server return _clientSessionsManager.UnsubscribeAsync(clientId, topicFilters); } - public Task PublishAsync(MqttApplicationMessage applicationMessage) + public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + MqttTopicValidator.ThrowIfInvalid(applicationMessage.Topic); + if (_cancellationTokenSource == null) throw new InvalidOperationException("The server is not started."); - _clientSessionsManager.EnqueueApplicationMessage(null, applicationMessage.ToPublishPacket()); + _clientSessionsManager.DispatchApplicationMessage(applicationMessage, null); - return Task.FromResult(0); + return Task.FromResult(new MqttClientPublishResult()); } public async Task StartAsync(IMqttServerOptions options) @@ -104,12 +127,17 @@ namespace MQTTnet.Server foreach (var adapter in _adapters) { - adapter.ClientAccepted += OnClientAccepted; + adapter.ClientHandler = OnHandleClient; await adapter.StartAsync(Options).ConfigureAwait(false); } _logger.Info("Started."); - Started?.Invoke(this, EventArgs.Empty); + + var startedHandler = StartedHandler; + if (startedHandler != null) + { + await startedHandler.HandleServerStartedAsync(EventArgs.Empty).ConfigureAwait(false); + } } public async Task StopAsync() @@ -121,18 +149,17 @@ namespace MQTTnet.Server return; } - _clientSessionsManager.Stop(); + await _clientSessionsManager.StopAsync().ConfigureAwait(false); _cancellationTokenSource.Cancel(false); foreach (var adapter in _adapters) { - adapter.ClientAccepted -= OnClientAccepted; + adapter.ClientHandler = null; await adapter.StopAsync().ConfigureAwait(false); } _logger.Info("Stopped."); - Stopped?.Invoke(this, EventArgs.Empty); } finally { @@ -144,16 +171,22 @@ namespace MQTTnet.Server _clientSessionsManager?.Dispose(); _clientSessionsManager = null; } + + var stoppedHandler = StoppedHandler; + if (stoppedHandler != null) + { + await stoppedHandler.HandleServerStoppedAsync(EventArgs.Empty).ConfigureAwait(false); + } } - public Task ClearRetainedMessagesAsync() + public Task ClearRetainedApplicationMessagesAsync() { return _retainedMessagesManager?.ClearMessagesAsync(); } - private void OnClientAccepted(object sender, MqttServerAdapterClientAcceptedEventArgs eventArgs) + private Task OnHandleClient(IMqttChannelAdapter channelAdapter) { - eventArgs.SessionTask = _clientSessionsManager.StartSession(eventArgs.Client); + return _clientSessionsManager.HandleClientAsync(channelAdapter); } } } diff --git a/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs new file mode 100644 index 00000000..82ba0f37 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerApplicationMessageInterceptorDelegate : IMqttServerApplicationMessageInterceptor + { + private readonly Func _callback; + + public MqttServerApplicationMessageInterceptorDelegate(Action callback) + { + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + _callback = context => + { + callback(context); + return Task.FromResult(0); + }; + } + + public MqttServerApplicationMessageInterceptorDelegate(Func callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + public Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context) + { + return _callback(context); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs similarity index 61% rename from Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs rename to Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs index 6f043b44..71c42c3e 100644 --- a/Source/MQTTnet/Server/MqttClientConnectedEventArgs.cs +++ b/Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs @@ -2,9 +2,9 @@ namespace MQTTnet.Server { - public class MqttClientConnectedEventArgs : EventArgs + public class MqttServerClientConnectedEventArgs : EventArgs { - public MqttClientConnectedEventArgs(string clientId) + public MqttServerClientConnectedEventArgs(string clientId) { ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); } diff --git a/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs new file mode 100644 index 00000000..2883d66b --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerClientConnectedHandlerDelegate : IMqttServerClientConnectedHandler + { + private readonly Func _handler; + + public MqttServerClientConnectedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerClientConnectedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleClientConnectedAsync(MqttServerClientConnectedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs b/Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs new file mode 100644 index 00000000..9e742749 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace MQTTnet.Server +{ + public class MqttServerClientDisconnectedEventArgs : EventArgs + { + public MqttServerClientDisconnectedEventArgs(string clientId, MqttClientDisconnectType disconnectType) + { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + DisconnectType = disconnectType; + } + + public string ClientId { get; } + + public MqttClientDisconnectType DisconnectType { get; } + } +} diff --git a/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs new file mode 100644 index 00000000..b1d495a6 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerClientDisconnectedHandlerDelegate : IMqttServerClientDisconnectedHandler + { + private readonly Func _handler; + + public MqttServerClientDisconnectedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerClientDisconnectedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleClientDisconnectedAsync(MqttServerClientDisconnectedEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientSubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs similarity index 67% rename from Source/MQTTnet/Server/MqttClientSubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs index 11f5c063..1454ac94 100644 --- a/Source/MQTTnet/Server/MqttClientSubscribedTopicEventArgs.cs +++ b/Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs @@ -2,9 +2,9 @@ namespace MQTTnet.Server { - public class MqttClientSubscribedTopicEventArgs : EventArgs + public class MqttServerClientSubscribedTopicEventArgs : EventArgs { - public MqttClientSubscribedTopicEventArgs(string clientId, TopicFilter topicFilter) + public MqttServerClientSubscribedTopicEventArgs(string clientId, TopicFilter topicFilter) { ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); diff --git a/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs new file mode 100644 index 00000000..048b0a8a --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerClientSubscribedHandlerDelegate : IMqttServerClientSubscribedTopicHandler + { + private readonly Func _handler; + + public MqttServerClientSubscribedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerClientSubscribedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleClientSubscribedTopicAsync(MqttServerClientSubscribedTopicEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttClientUnSubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs similarity index 67% rename from Source/MQTTnet/Server/MqttClientUnSubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs index d76347e0..bb78bb28 100644 --- a/Source/MQTTnet/Server/MqttClientUnSubscribedTopicEventArgs.cs +++ b/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs @@ -2,9 +2,9 @@ namespace MQTTnet.Server { - public class MqttClientUnsubscribedTopicEventArgs : EventArgs + public class MqttServerClientUnsubscribedTopicEventArgs : EventArgs { - public MqttClientUnsubscribedTopicEventArgs(string clientId, string topicFilter) + public MqttServerClientUnsubscribedTopicEventArgs(string clientId, string topicFilter) { ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); diff --git a/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs new file mode 100644 index 00000000..44162876 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerClientUnsubscribedTopicHandlerDelegate : IMqttServerClientUnsubscribedTopicHandler + { + private readonly Func _handler; + + public MqttServerClientUnsubscribedTopicHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerClientUnsubscribedTopicHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleClientUnsubscribedTopicAsync(MqttServerClientUnsubscribedTopicEventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs b/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs new file mode 100644 index 00000000..8be08c8d --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerConnectionValidatorDelegate : IMqttServerConnectionValidator + { + private readonly Func _callback; + + public MqttServerConnectionValidatorDelegate(Action callback) + { + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + _callback = context => + { + callback(context); + return Task.FromResult(0); + }; + } + + public MqttServerConnectionValidatorDelegate(Func callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + public Task ValidateConnectionAsync(MqttConnectionValidatorContext context) + { + return _callback(context); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerEventDispatcher.cs b/Source/MQTTnet/Server/MqttServerEventDispatcher.cs index 8fd5652d..c85805d8 100644 --- a/Source/MQTTnet/Server/MqttServerEventDispatcher.cs +++ b/Source/MQTTnet/Server/MqttServerEventDispatcher.cs @@ -1,42 +1,73 @@ -using System; +using System.Threading.Tasks; +using MQTTnet.Client.Receiving; namespace MQTTnet.Server { public class MqttServerEventDispatcher { - public event EventHandler ClientSubscribedTopic; + public IMqttServerClientConnectedHandler ClientConnectedHandler { get; set; } - public event EventHandler ClientUnsubscribedTopic; + public IMqttServerClientDisconnectedHandler ClientDisconnectedHandler { get; set; } - public event EventHandler ClientConnected; + public IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler { get; set; } - public event EventHandler ClientDisconnected; + public IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler { get; set; } - public event EventHandler ApplicationMessageReceived; + public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } - public void OnClientSubscribedTopic(string clientId, TopicFilter topicFilter) + public Task HandleClientConnectedAsync(string clientId) { - ClientSubscribedTopic?.Invoke(this, new MqttClientSubscribedTopicEventArgs(clientId, topicFilter)); + var handler = ClientConnectedHandler; + if (handler == null) + { + return Task.FromResult(0); + } + + return handler.HandleClientConnectedAsync(new MqttServerClientConnectedEventArgs(clientId)); } - public void OnClientUnsubscribedTopic(string clientId, string topicFilter) + public Task HandleClientDisconnectedAsync(string clientId, MqttClientDisconnectType disconnectType) { - ClientUnsubscribedTopic?.Invoke(this, new MqttClientUnsubscribedTopicEventArgs(clientId, topicFilter)); + var handler = ClientDisconnectedHandler; + if (handler == null) + { + return Task.FromResult(0); + } + + return handler.HandleClientDisconnectedAsync(new MqttServerClientDisconnectedEventArgs(clientId, disconnectType)); } - public void OnClientDisconnected(string clientId, bool wasCleanDisconnect) + public Task HandleClientSubscribedTopicAsync(string clientId, TopicFilter topicFilter) { - ClientDisconnected?.Invoke(this, new MqttClientDisconnectedEventArgs(clientId, wasCleanDisconnect)); + var handler = ClientSubscribedTopicHandler; + if (handler == null) + { + return Task.FromResult(0); + } + + return handler.HandleClientSubscribedTopicAsync(new MqttServerClientSubscribedTopicEventArgs(clientId, topicFilter)); } - public void OnApplicationMessageReceived(string senderClientId, MqttApplicationMessage applicationMessage) + public Task HandleClientUnsubscribedTopicAsync(string clientId, string topicFilter) { - ApplicationMessageReceived?.Invoke(this, new MqttApplicationMessageReceivedEventArgs(senderClientId, applicationMessage)); + var handler = ClientUnsubscribedTopicHandler; + if (handler == null) + { + return Task.FromResult(0); + } + + return handler.HandleClientUnsubscribedTopicAsync(new MqttServerClientUnsubscribedTopicEventArgs(clientId, topicFilter)); } - public void OnClientConnected(string clientId) + public Task HandleApplicationMessageReceivedAsync(string senderClientId, MqttApplicationMessage applicationMessage) { - ClientConnected?.Invoke(this, new MqttClientConnectedEventArgs(clientId)); + var handler = ApplicationMessageReceivedHandler; + if (handler == null) + { + return Task.FromResult(0); + } + + return handler.HandleApplicationMessageReceivedAsync(new MqttApplicationMessageReceivedEventArgs(senderClientId, applicationMessage)); } } } diff --git a/Source/MQTTnet/Server/MqttServerExtensions.cs b/Source/MQTTnet/Server/MqttServerExtensions.cs new file mode 100644 index 00000000..c38aac76 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerExtensions.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public static class MqttServerExtensions + { + public static IMqttServer UseClientConnectedHandler(this IMqttServer server, Func handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseClientConnectedHandler((IMqttServerClientConnectedHandler)null); + } + + return server.UseClientConnectedHandler(new MqttServerClientConnectedHandlerDelegate(handler)); + } + + public static IMqttServer UseClientConnectedHandler(this IMqttServer server, Action handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseClientConnectedHandler((IMqttServerClientConnectedHandler)null); + } + + return server.UseClientConnectedHandler(new MqttServerClientConnectedHandlerDelegate(handler)); + } + + public static IMqttServer UseClientConnectedHandler(this IMqttServer server, IMqttServerClientConnectedHandler handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + server.ClientConnectedHandler = handler; + return server; + } + + public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, Func handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseClientDisconnectedHandler((IMqttServerClientDisconnectedHandler)null); + } + + return server.UseClientDisconnectedHandler(new MqttServerClientDisconnectedHandlerDelegate(handler)); + } + + public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, Action handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseClientDisconnectedHandler((IMqttServerClientDisconnectedHandler)null); + } + + return server.UseClientDisconnectedHandler(new MqttServerClientDisconnectedHandlerDelegate(handler)); + } + + public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, IMqttServerClientDisconnectedHandler handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + server.ClientDisconnectedHandler = handler; + return server; + } + + public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, Func handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return server.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, Action handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + if (handler == null) + { + return server.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + } + + return server.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + } + + public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, IMqttApplicationMessageReceivedHandler handler) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + server.ApplicationMessageReceivedHandler = handler; + return server; + } + + public static Task SubscribeAsync(this IMqttServer server, string clientId, params TopicFilter[] 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 IMqttServer server, string clientId, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.SubscribeAsync(clientId, new TopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + } + + public static Task SubscribeAsync(this IMqttServer 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 TopicFilterBuilder().WithTopic(topic).Build()); + } + + public static Task UnsubscribeAsync(this IMqttServer server, string clientId, params string[] topicFilters) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return server.UnsubscribeAsync(clientId, topicFilters); + } + + public static async Task PublishAsync(this IMqttServer server, IEnumerable applicationMessages) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await server.PublishAsync(applicationMessage).ConfigureAwait(false); + } + } + + public static Task PublishAsync(this IMqttServer server, MqttApplicationMessage applicationMessage) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + return server.PublishAsync(applicationMessage, CancellationToken.None); + } + + public static async Task PublishAsync(this IMqttServer server, params MqttApplicationMessage[] applicationMessages) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); + + foreach (var applicationMessage in applicationMessages) + { + await server.PublishAsync(applicationMessage, CancellationToken.None).ConfigureAwait(false); + } + } + + public static Task PublishAsync(this IMqttServer server, string topic) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.PublishAsync(builder => builder + .WithTopic(topic)); + } + + public static Task PublishAsync(this IMqttServer server, string topic, string payload) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload)); + } + + public static Task PublishAsync(this IMqttServer server, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel)); + } + + public static Task PublishAsync(this IMqttServer server, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.PublishAsync(builder => builder + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .WithRetainFlag(retain)); + } + + public static Task PublishAsync(this IMqttServer server, Func builder, CancellationToken cancellationToken) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + var message = builder(new MqttApplicationMessageBuilder()).Build(); + return server.PublishAsync(message, cancellationToken); + } + + public static Task PublishAsync(this IMqttServer server, Func builder) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + var message = builder(new MqttApplicationMessageBuilder()).Build(); + return server.PublishAsync(message, CancellationToken.None); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerOptions.cs b/Source/MQTTnet/Server/MqttServerOptions.cs index 6c7c4f94..7147ef84 100644 --- a/Source/MQTTnet/Server/MqttServerOptions.cs +++ b/Source/MQTTnet/Server/MqttServerOptions.cs @@ -8,20 +8,23 @@ namespace MQTTnet.Server public MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsTcpEndpointOptions(); + public string ClientId { get; set; } + public bool EnablePersistentSessions { get; set; } public int MaxPendingMessagesPerClient { get; set; } = 250; + public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage; public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(15); - public Action ConnectionValidator { get; set; } + public IMqttServerConnectionValidator ConnectionValidator { get; set; } - public Action ApplicationMessageInterceptor { get; set; } + public IMqttServerApplicationMessageInterceptor ApplicationMessageInterceptor { get; set; } + + public IMqttServerClientMessageQueueInterceptor ClientMessageQueueInterceptor { get; set; } - public Action ClientMessageQueueInterceptor { get; set; } - - public Action SubscriptionInterceptor { get; set; } + public IMqttServerSubscriptionInterceptor SubscriptionInterceptor { get; set; } public IMqttServerStorage Storage { get; set; } } diff --git a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs index 45400620..357cb82d 100644 --- a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs @@ -75,6 +75,12 @@ namespace MQTTnet.Server return this; } + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value) + { + _options.TlsEndpointOptions.BoundInterNetworkV6Address = value; + return this; + } + public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value) { _options.TlsEndpointOptions.Certificate = value; @@ -99,30 +105,57 @@ namespace MQTTnet.Server return this; } - public MqttServerOptionsBuilder WithConnectionValidator(Action value) + public MqttServerOptionsBuilder WithConnectionValidator(IMqttServerConnectionValidator value) { _options.ConnectionValidator = value; return this; } - public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Action value) + public MqttServerOptionsBuilder WithConnectionValidator(Action value) + { + _options.ConnectionValidator = new MqttServerConnectionValidatorDelegate(value); + return this; + } + + public MqttServerOptionsBuilder WithApplicationMessageInterceptor(IMqttServerApplicationMessageInterceptor value) { _options.ApplicationMessageInterceptor = value; return this; } - public MqttServerOptionsBuilder WithSubscriptionInterceptor(Action value) + public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Action value) + { + _options.ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); + return this; + } + + public MqttServerOptionsBuilder WithSubscriptionInterceptor(IMqttServerSubscriptionInterceptor value) { _options.SubscriptionInterceptor = value; return this; } + public MqttServerOptionsBuilder WithSubscriptionInterceptor(Action value) + { + _options.SubscriptionInterceptor = new MqttServerSubscriptionInterceptorDelegate(value); + return this; + } + public MqttServerOptionsBuilder WithPersistentSessions() { _options.EnablePersistentSessions = true; return this; } + /// + /// Gets or sets the client ID which is used when publishing messages from the server directly. + /// + public MqttServerOptionsBuilder WithClientId(string value) + { + _options.ClientId = value; + return this; + } + public IMqttServerOptions Build() { return _options; diff --git a/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs new file mode 100644 index 00000000..8a053574 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerStartedHandlerDelegate : IMqttServerStartedHandler + { + private readonly Func _handler; + + public MqttServerStartedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerStartedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleServerStartedAsync(EventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs new file mode 100644 index 00000000..14e6a621 --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerStoppedHandlerDelegate : IMqttServerStoppedHandler + { + private readonly Func _handler; + + public MqttServerStoppedHandlerDelegate(Action handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + + _handler = eventArgs => + { + handler(eventArgs); + return Task.FromResult(0); + }; + } + + public MqttServerStoppedHandlerDelegate(Func handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + public Task HandleServerStoppedAsync(EventArgs eventArgs) + { + return _handler(eventArgs); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs new file mode 100644 index 00000000..f500e07e --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server +{ + public class MqttServerSubscriptionInterceptorDelegate : IMqttServerSubscriptionInterceptor + { + private readonly Func _callback; + + public MqttServerSubscriptionInterceptorDelegate(Action callback) + { + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + _callback = context => + { + callback(context); + return Task.FromResult(0); + }; + } + + public MqttServerSubscriptionInterceptorDelegate(Func callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + public Task InterceptSubscriptionAsync(MqttSubscriptionInterceptorContext context) + { + return _callback(context); + } + } +} diff --git a/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs b/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs index 45df2780..01b8c704 100644 --- a/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs +++ b/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs @@ -10,6 +10,8 @@ namespace MQTTnet.Server public int ConnectionBacklog { get; set; } = 10; + public bool NoDelay { get; set; } = true; + #if WINDOWS_UWP public int BufferSize { get; set; } = 4096; #endif diff --git a/Source/MQTTnet/Server/MqttSubscribeResult.cs b/Source/MQTTnet/Server/MqttSubscribeResult.cs deleted file mode 100644 index 2de40046..00000000 --- a/Source/MQTTnet/Server/MqttSubscribeResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MQTTnet.Packets; - -namespace MQTTnet.Server -{ - public class MqttSubscribeResult - { - public MqttSubAckPacket ResponsePacket { get; set; } - - public bool CloseConnection { get; set; } - } -} diff --git a/Source/MQTTnet/Server/PrepareClientSessionResult.cs b/Source/MQTTnet/Server/PrepareClientSessionResult.cs index 9a655bec..75090376 100644 --- a/Source/MQTTnet/Server/PrepareClientSessionResult.cs +++ b/Source/MQTTnet/Server/PrepareClientSessionResult.cs @@ -4,6 +4,6 @@ { public bool IsExistingSession { get; set; } - public MqttClientSession Session { get; set; } + public MqttClientConnection Session { get; set; } } } diff --git a/Source/MQTTnet/Server/Status/IMqttClientStatus.cs b/Source/MQTTnet/Server/Status/IMqttClientStatus.cs new file mode 100644 index 00000000..714b00db --- /dev/null +++ b/Source/MQTTnet/Server/Status/IMqttClientStatus.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Formatter; + +namespace MQTTnet.Server.Status +{ + public interface IMqttClientStatus + { + string ClientId { get; } + + string Endpoint { get; } + + MqttProtocolVersion ProtocolVersion { get; } + + DateTime LastPacketReceivedTimestamp { get; } + + DateTime LastNonKeepAlivePacketReceivedTimestamp { get; } + + long ReceivedApplicationMessagesCount { get; } + + long SentApplicationMessagesCount { get; } + + long ReceivedPacketsCount { get; } + + long SentPacketsCount { get; } + + IMqttSessionStatus Session { get; } + + long BytesSent { get; } + + long BytesReceived { get; } + + Task DisconnectAsync(); + } +} diff --git a/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs b/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs new file mode 100644 index 00000000..349f715e --- /dev/null +++ b/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Server.Status +{ + public interface IMqttSessionStatus + { + string ClientId { get; set; } + + long PendingApplicationMessagesCount { get; set; } + + Task ClearPendingApplicationMessagesAsync(); + + Task DeleteAsync(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Status/MqttClientStatus.cs b/Source/MQTTnet/Server/Status/MqttClientStatus.cs new file mode 100644 index 00000000..cc6e77d3 --- /dev/null +++ b/Source/MQTTnet/Server/Status/MqttClientStatus.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using MQTTnet.Formatter; + +namespace MQTTnet.Server.Status +{ + public class MqttClientStatus : IMqttClientStatus + { + private readonly MqttClientConnection _connection; + + public MqttClientStatus(MqttClientConnection connection) + { + _connection = connection; + } + + public string ClientId { get; set; } + + public string Endpoint { get; set; } + + public MqttProtocolVersion ProtocolVersion { get; set; } + + public DateTime LastPacketReceivedTimestamp { get; set; } + + public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; set; } + + public long ReceivedApplicationMessagesCount { get; set; } + + public long SentApplicationMessagesCount { get; set; } + + public long ReceivedPacketsCount { get; set; } + + public long SentPacketsCount { get; set; } + + public IMqttSessionStatus Session { get; set; } + + public long BytesSent { get; set; } + + public long BytesReceived { get; set; } + + public Task DisconnectAsync() + { + return _connection.StopAsync(); + } + } +} diff --git a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs new file mode 100644 index 00000000..451644c6 --- /dev/null +++ b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace MQTTnet.Server.Status +{ + public class MqttSessionStatus : IMqttSessionStatus + { + private readonly MqttClientSession _session; + private readonly MqttClientSessionsManager _sessionsManager; + + public MqttSessionStatus(MqttClientSession session, MqttClientSessionsManager sessionsManager) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); + } + + public string ClientId { get; set; } + + public long PendingApplicationMessagesCount { get; set; } + + public DateTime CreatedTimestamp { get; set; } + + public Task DeleteAsync() + { + return _sessionsManager.DeleteSessionAsync(ClientId); + } + + public Task ClearPendingApplicationMessagesAsync() + { + _session.ApplicationMessagesQueue.Clear(); + return Task.FromResult(0); + } + } +} diff --git a/Source/MQTTnet/TopicFilter.cs b/Source/MQTTnet/TopicFilter.cs index 7eebbc2c..3b62e7e0 100644 --- a/Source/MQTTnet/TopicFilter.cs +++ b/Source/MQTTnet/TopicFilter.cs @@ -4,19 +4,39 @@ namespace MQTTnet { public class TopicFilter { - public TopicFilter(string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - Topic = topic; - QualityOfServiceLevel = qualityOfServiceLevel; - } - public string Topic { get; set; } public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + /// + /// This is only supported when using MQTTv5. + /// + public bool? NoLocal { get; set; } + + /// + /// This is only supported when using MQTTv5. + /// + public bool? RetainAsPublished { get; set; } + + /// + /// This is only supported when using MQTTv5. + /// + public MqttRetainHandling? RetainHandling { get; set; } + public override string ToString() { - return Topic + "@" + QualityOfServiceLevel; + return string.Concat( + "TopicFilter: [Topic=", + Topic, + "] [QualityOfServiceLevel=", + QualityOfServiceLevel, + "] [NoLocal=", + NoLocal, + "] [RetainAsPublished=", + RetainAsPublished, + "] [RetainHandling=", + RetainHandling, + "]"); } } } \ No newline at end of file diff --git a/Source/MQTTnet/TopicFilterBuilder.cs b/Source/MQTTnet/TopicFilterBuilder.cs index 38c8fbc2..a4802637 100644 --- a/Source/MQTTnet/TopicFilterBuilder.cs +++ b/Source/MQTTnet/TopicFilterBuilder.cs @@ -45,7 +45,7 @@ namespace MQTTnet throw new MqttProtocolViolationException("Topic is not set."); } - return new TopicFilter(_topic, _qualityOfServiceLevel); + return new TopicFilter { Topic = _topic, QualityOfServiceLevel = _qualityOfServiceLevel }; } } } diff --git a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj index 998acd2f..06f0e5f1 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj +++ b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj @@ -7,13 +7,14 @@ - - - + + + + diff --git a/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs b/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs index 699aa65b..8fb74ae8 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs +++ b/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs @@ -3,11 +3,11 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.AspNetCore.Tests.Mockups; using MQTTnet.Exceptions; using MQTTnet.Packets; -using MQTTnet.Serializer; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Formatter; namespace MQTTnet.AspNetCore.Tests { @@ -17,7 +17,7 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() { - var serializer = new MqttPacketSerializer {}; + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); var pipe = new DuplexPipeMockup(); var connection = new DefaultConnectionContext(); connection.Transport = pipe; @@ -25,13 +25,13 @@ namespace MQTTnet.AspNetCore.Tests pipe.Receive.Writer.Complete(); - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(TimeSpan.FromSeconds(1), CancellationToken.None)); + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(TimeSpan.Zero, CancellationToken.None)); } [TestMethod] public async Task TestParallelWrites() { - var serializer = new MqttPacketSerializer { }; + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); var pipe = new DuplexPipeMockup(); var connection = new DefaultConnectionContext(); connection.Transport = pipe; @@ -41,7 +41,7 @@ namespace MQTTnet.AspNetCore.Tests { for (int i = 0; i < 100; i++) { - await ctx.SendPacketAsync(new MqttPublishPacket(), CancellationToken.None).ConfigureAwait(false); + await ctx.SendPacketAsync(new MqttPublishPacket(), TimeSpan.Zero, CancellationToken.None).ConfigureAwait(false); } })); diff --git a/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs b/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs new file mode 100644 index 00000000..2d389ab1 --- /dev/null +++ b/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; +using MQTTnet.Tests; + +namespace MQTTnet.AspNetCore.Tests +{ + [TestClass] + public class MqttPacketSerializerTestsWithSpanBasedReader : MqttPacketSerializer_Tests + { + protected override IMqttPacketBodyReader ReaderFactory(byte[] data) + { + var result = new SpanBasedMqttPacketBodyReader(); + result.SetBuffer(data); + return result; + } + + protected override IMqttPacketWriter WriterFactory() + { + return new SpanBasedMqttPacketWriter(); + } + } +} diff --git a/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs b/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs index e6cdef23..f08f57df 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs +++ b/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs @@ -1,8 +1,8 @@ +#if NETCOREAPP using System.Buffers; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.AspNetCore; +using MQTTnet.Formatter; using MQTTnet.Packets; -using MQTTnet.Serializer; namespace MQTTnet.AspNetCore.Tests { @@ -12,9 +12,9 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public void TestTryDeserialize() { - var serializer = new MqttPacketSerializer(); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); - var buffer = serializer.Serialize(new MqttPublishPacket() {Topic = "a", Payload = new byte[5]}); + var buffer = serializer.Encode(new MqttPublishPacket() {Topic = "a", Payload = new byte[5]}); var sequence = new ReadOnlySequence(buffer.Array, buffer.Offset, buffer.Count); @@ -23,23 +23,27 @@ namespace MQTTnet.AspNetCore.Tests var consumed = part.Start; var observed = part.Start; var result = false; + var read = 0; + + var reader = new SpanBasedMqttPacketBodyReader(); part = sequence.Slice(sequence.Start, 0); // empty message should fail - result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 1); // partial fixed header should fail - result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 4); // partial body should fail - result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence; // complete msg should work - result = serializer.TryDeserialize(part, out packet, out consumed, out observed); + result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); Assert.IsTrue(result); } } } +#endif \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs b/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs index 7f068d60..3a5c6961 100644 --- a/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs @@ -3,10 +3,10 @@ using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Internal; using MQTTnet.Packets; -using MQTTnet.Serializer; using System; using System.IO; using System.Threading; +using MQTTnet.Formatter; namespace MQTTnet.Benchmarks { @@ -21,13 +21,14 @@ namespace MQTTnet.Benchmarks [GlobalSetup] public void Setup() { - var message = new MqttApplicationMessageBuilder() - .WithTopic("A") - .Build(); + _packet = new MqttPublishPacket + { + Topic = "A" + }; - _packet = message.ToPublishPacket(); - var serializer = new MqttPacketSerializer(); - var serializedPacket = Join(serializer.Serialize(_packet)); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + + var serializedPacket = Join(serializer.Encode(_packet)); _iterations = 10000; @@ -65,7 +66,7 @@ namespace MQTTnet.Benchmarks for (var i = 0; i < 10000; i++) { - _channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult(); + _channelAdapter.SendPacketAsync(_packet, TimeSpan.Zero, CancellationToken.None).GetAwaiter().GetResult(); } _stream.Position = 0; diff --git a/Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs b/Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs similarity index 54% rename from Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs rename to Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs index ecb31a20..076d1e8d 100644 --- a/Tests/MQTTnet.Benchmarks/AllowNonOptimized.cs +++ b/Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs @@ -3,20 +3,17 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Validators; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks.Configurations { /// /// this options may be used to run benchmarks in debugmode and attach a performance profiler /// https://benchmarkdotnet.org/Configs/Configs.htm /// - public class AllowNonOptimized : ManualConfig + public class AllowNonOptimized : BaseConfig { public AllowNonOptimized() { Add(JitOptimizationsValidator.DontFailOnError); // ALLOW NON-OPTIMIZED DLLS - Add(DefaultConfig.Instance.GetLoggers().ToArray()); // manual config has no loggers by default - Add(DefaultConfig.Instance.GetExporters().ToArray()); // manual config has no exporters by default - Add(DefaultConfig.Instance.GetColumnProviders().ToArray()); // manual config has no columns by default Add(Job.InProcess); } } diff --git a/Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs b/Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs new file mode 100644 index 00000000..418bb2f3 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs @@ -0,0 +1,15 @@ +using BenchmarkDotNet.Configs; +using System.Linq; + +namespace MQTTnet.Benchmarks.Configurations +{ + public class BaseConfig : ManualConfig + { + public BaseConfig() + { + Add(DefaultConfig.Instance.GetLoggers().ToArray()); // manual config has no loggers by default + Add(DefaultConfig.Instance.GetExporters().ToArray()); // manual config has no exporters by default + Add(DefaultConfig.Instance.GetColumnProviders().ToArray()); // manual config has no columns by default + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs b/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs new file mode 100644 index 00000000..608271e2 --- /dev/null +++ b/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs @@ -0,0 +1,17 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.CsProj; + +namespace MQTTnet.Benchmarks.Configurations +{ + public class RuntimeCompareConfig : BaseConfig + { + public RuntimeCompareConfig() + { + Add(Job.Default.With(Runtime.Clr)); + Add(Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.NetCoreApp21)); + } + + } +} diff --git a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj index bfd2c392..03aabc85 100644 --- a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj +++ b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj @@ -3,15 +3,15 @@ Exe Full - net461 + net461;netcoreapp2.1 netcoreapp2.1 7.2 - + - + diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs index 5e99201c..c821ee07 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Server; namespace MQTTnet.Benchmarks diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs b/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs index 1e2fbb2b..7bd085c2 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Hosting; using MQTTnet.Server; using MQTTnet.Diagnostics; using MQTTnet.AspNetCore.Client; +using MQTTnet.Client.Options; namespace MQTTnet.Benchmarks { diff --git a/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs b/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs index 1f29c97a..04761e79 100644 --- a/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs @@ -1,11 +1,11 @@ using BenchmarkDotNet.Attributes; using MQTTnet.Channel; -using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Implementations; using MQTTnet.Server; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client.Options; namespace MQTTnet.Benchmarks { @@ -21,8 +21,16 @@ namespace MQTTnet.Benchmarks { var factory = new MqttFactory(); var tcpServer = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); - tcpServer.ClientAccepted += (sender, args) => _serverChannel = (IMqttChannel)args.Client.GetType().GetField("_channel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(args.Client); + tcpServer.ClientHandler += args => + { + _serverChannel = + (IMqttChannel)args.GetType().GetField("_channel", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .GetValue(args); + return Task.CompletedTask; + }; + _mqttServer = factory.CreateMqttServer(new[] { tcpServer }, new MqttNetLogger()); var serverOptions = new MqttServerOptionsBuilder().Build(); diff --git a/Tests/MQTTnet.Benchmarks/Program.cs b/Tests/MQTTnet.Benchmarks/Program.cs index 9407ccfe..280416d3 100644 --- a/Tests/MQTTnet.Benchmarks/Program.cs +++ b/Tests/MQTTnet.Benchmarks/Program.cs @@ -1,5 +1,6 @@ using System; using BenchmarkDotNet.Running; +using MQTTnet.Benchmarks.Configurations; using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks @@ -43,7 +44,7 @@ namespace MQTTnet.Benchmarks BenchmarkRunner.Run(); break; case '8': - BenchmarkRunner.Run(new AllowNonOptimized()); + BenchmarkRunner.Run(new RuntimeCompareConfig()/*new AllowNonOptimized()*/); break; } diff --git a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs index ed18df1d..c8b3f7c6 100644 --- a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -1,12 +1,12 @@ using BenchmarkDotNet.Attributes; using MQTTnet.Packets; -using MQTTnet.Serializer; -using MQTTnet.Internal; using System; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Channel; +using MQTTnet.Formatter; +using MQTTnet.Formatter.V3; namespace MQTTnet.Benchmarks { @@ -17,18 +17,18 @@ namespace MQTTnet.Benchmarks { private MqttBasePacket _packet; private ArraySegment _serializedPacket; - private MqttPacketSerializer _serializer; + private IMqttPacketFormatter _serializer; [GlobalSetup] public void Setup() { - var message = new MqttApplicationMessageBuilder() - .WithTopic("A") - .Build(); + _packet = new MqttPublishPacket + { + Topic = "A" + }; - _packet = message.ToPublishPacket(); - _serializer = new MqttPacketSerializer(); - _serializedPacket = _serializer.Serialize(_packet); + _serializer = new MqttV311PacketFormatter(); + _serializedPacket = _serializer.Encode(_packet); } [Benchmark] @@ -36,7 +36,7 @@ namespace MQTTnet.Benchmarks { for (var i = 0; i < 10000; i++) { - _serializer.Serialize(_packet); + _serializer.Encode(_packet); _serializer.FreeBuffer(); } } @@ -46,19 +46,19 @@ namespace MQTTnet.Benchmarks { var channel = new BenchmarkMqttChannel(_serializedPacket); var fixedHeader = new byte[2]; - var singleByteBuffer = new byte[1]; + var reader = new MqttPacketReader(channel); for (var i = 0; i < 10000; i++) { channel.Reset(); - var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + var header = reader.ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; var receivedPacket = new ReceivedMqttPacket( header.Flags, - new MqttPacketBodyReader(_serializedPacket.Array, _serializedPacket.Count - header.RemainingLength, _serializedPacket.Array.Length)); + new MqttPacketBodyReader(_serializedPacket.Array, (ulong)(_serializedPacket.Count - header.RemainingLength), (ulong)_serializedPacket.Array.Length), 0); - _serializer.Deserialize(receivedPacket); + _serializer.Decode(receivedPacket); } } @@ -73,7 +73,9 @@ namespace MQTTnet.Benchmarks _position = _buffer.Offset; } - public string Endpoint { get; } + public string Endpoint { get; } = string.Empty; + + public bool IsSecureConnection { get; } = false; public void Reset() { @@ -82,12 +84,12 @@ namespace MQTTnet.Benchmarks public Task ConnectAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotSupportedException(); } - public Task DisconnectAsync() + public Task DisconnectAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -100,7 +102,7 @@ namespace MQTTnet.Benchmarks public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void Dispose() diff --git a/Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs b/Tests/MQTTnet.Core.Tests/AsyncAutoResentEvent_Tests.cs similarity index 95% rename from Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs rename to Tests/MQTTnet.Core.Tests/AsyncAutoResentEvent_Tests.cs index 074b0099..b2b1e50b 100644 --- a/Tests/MQTTnet.Core.Tests/AsyncAutoResentEventTests.cs +++ b/Tests/MQTTnet.Core.Tests/AsyncAutoResentEvent_Tests.cs @@ -1,18 +1,18 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Internal; -using System; +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] // Inspired from the vs-threading tests (https://github.com/Microsoft/vs-threading/blob/master/src/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs) - public class AsyncAutoResetEventTests + public class AsyncAutoResetEvent_Tests { private readonly AsyncAutoResetEvent _aare; - public AsyncAutoResetEventTests() + public AsyncAutoResetEvent_Tests() { _aare = new AsyncAutoResetEvent(); } @@ -123,13 +123,12 @@ namespace MQTTnet.Core.Tests } catch (OperationCanceledException ex) { - Assert.AreEqual(cts.Token, ex.CancellationToken); } // Now set the event and verify that a future waiter gets the signal immediately. _aare.Set(); waitTask = _aare.WaitOneAsync(); - Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); + Assert.AreEqual(TaskStatus.WaitingForActivation, waitTask.Status); } [TestMethod] diff --git a/Tests/MQTTnet.Core.Tests/AsyncLockTests.cs b/Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs similarity index 90% rename from Tests/MQTTnet.Core.Tests/AsyncLockTests.cs rename to Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs index 43f2486c..ad1d10cd 100644 --- a/Tests/MQTTnet.Core.Tests/AsyncLockTests.cs +++ b/Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs @@ -3,10 +3,10 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class AsyncLockTests + public class AsyncLock_Tests { [TestMethod] public void AsyncLock() @@ -21,7 +21,7 @@ namespace MQTTnet.Core.Tests #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed threads[i] = Task.Run(async () => { - using (var releaser = await @lock.LockAsync(CancellationToken.None)) + using (var releaser = await @lock.WaitAsync(CancellationToken.None)) { var localI = globalI; await Task.Delay(10); // Increase the chance for wrong data. diff --git a/Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs b/Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs new file mode 100644 index 00000000..c222cdff --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs @@ -0,0 +1,97 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; + +namespace MQTTnet.Tests +{ + [TestClass] + public class AsyncQueue_Tests + { + [TestMethod] + public async Task Preserve_Order() + { + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); + + Assert.AreEqual("1", (await queue.TryDequeueAsync(CancellationToken.None)).Item); + Assert.AreEqual("2", (await queue.TryDequeueAsync(CancellationToken.None)).Item); + Assert.AreEqual("3", (await queue.TryDequeueAsync(CancellationToken.None)).Item); + } + + [TestMethod] + public void Count() + { + var queue = new AsyncQueue(); + + queue.Enqueue("1"); + Assert.AreEqual(1, queue.Count); + + queue.Enqueue("2"); + Assert.AreEqual(2, queue.Count); + + queue.Enqueue("3"); + Assert.AreEqual(3, queue.Count); + } + + + [TestMethod] + public async Task Preserve_ProcessAsync() + { + var queue = new AsyncQueue(); + + var sum = 0; + var worker = Task.Run(async () => + { + while (sum < 6) + { + sum += (await queue.TryDequeueAsync(CancellationToken.None)).Item; + } + }); + + queue.Enqueue(1); + await Task.Delay(500); + + queue.Enqueue(2); + await Task.Delay(500); + + queue.Enqueue(3); + await Task.Delay(500); + + Assert.AreEqual(6, sum); + Assert.AreEqual(TaskStatus.RanToCompletion, worker.Status); + } + + [TestMethod] + public void Dequeue_Sync() + { + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); + + Assert.AreEqual("1", queue.TryDequeue().Item); + Assert.AreEqual("2", queue.TryDequeue().Item); + Assert.AreEqual("3", queue.TryDequeue().Item); + } + + [TestMethod] + public void Clear() + { + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); + + queue.Clear(); + Assert.AreEqual(0, queue.Count); + + queue.Enqueue("4"); + + Assert.AreEqual(1, queue.Count); + Assert.AreEqual("4", queue.TryDequeue().Item); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs b/Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs similarity index 97% rename from Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs rename to Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs index 80e0d724..da4069af 100644 --- a/Tests/MQTTnet.Core.Tests/BlockingQueueTests.cs +++ b/Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs @@ -3,10 +3,10 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class BlockingQueueTests + public class BlockingQueue_Tests { [TestMethod] public void Preserve_Order() diff --git a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs b/Tests/MQTTnet.Core.Tests/Extension_Tests.cs similarity index 67% rename from Tests/MQTTnet.Core.Tests/ExtensionTests.cs rename to Tests/MQTTnet.Core.Tests/Extension_Tests.cs index fdb5888e..0eaa5e0e 100644 --- a/Tests/MQTTnet.Core.Tests/ExtensionTests.cs +++ b/Tests/MQTTnet.Core.Tests/Extension_Tests.cs @@ -4,30 +4,36 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; +using MQTTnet.Internal; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class ExtensionTests + public class Extension_Tests { [ExpectedException(typeof(MqttCommunicationTimedOutException))] [TestMethod] public async Task TimeoutAfter() { - await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct), TimeSpan.FromMilliseconds(100), CancellationToken.None); + await MqttTaskTimeout.WaitAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct), + TimeSpan.FromMilliseconds(100), CancellationToken.None); } [ExpectedException(typeof(MqttCommunicationTimedOutException))] [TestMethod] public async Task TimeoutAfterWithResult() { - await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct).ContinueWith(t => 5, ct), TimeSpan.FromMilliseconds(100), CancellationToken.None); + await MqttTaskTimeout.WaitAsync( + ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct).ContinueWith(t => 5, ct), + TimeSpan.FromMilliseconds(100), CancellationToken.None); } [TestMethod] public async Task TimeoutAfterCompleteInTime() { - var result = await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(100), ct).ContinueWith(t => 5, ct), TimeSpan.FromMilliseconds(500), CancellationToken.None); + var result = await MqttTaskTimeout.WaitAsync( + ct => Task.Delay(TimeSpan.FromMilliseconds(100), ct).ContinueWith(t => 5, ct), + TimeSpan.FromMilliseconds(500), CancellationToken.None); Assert.AreEqual(5, result); } @@ -36,7 +42,7 @@ namespace MQTTnet.Core.Tests { try { - await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Run(() => + await MqttTaskTimeout.WaitAsync(ct => Task.Run(() => { var iis = new int[0]; iis[1] = 0; @@ -55,7 +61,7 @@ namespace MQTTnet.Core.Tests { try { - await MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Run(() => + await MqttTaskTimeout.WaitAsync(ct => Task.Run(() => { var iis = new int[0]; iis[1] = 0; @@ -76,7 +82,8 @@ namespace MQTTnet.Core.Tests var tasks = Enumerable.Range(0, 100000) .Select(i => { - return MQTTnet.Internal.TaskExtensions.TimeoutAfterAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(1), ct), TimeSpan.FromMinutes(1), CancellationToken.None); + return MqttTaskTimeout.WaitAsync(ct => Task.Delay(TimeSpan.FromMilliseconds(1), ct), + TimeSpan.FromMinutes(1), CancellationToken.None); }); await Task.WhenAll(tasks); @@ -91,4 +98,4 @@ namespace MQTTnet.Core.Tests } } } -} +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj b/Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj similarity index 74% rename from Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj rename to Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj index 2baaa286..74937a9d 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTnet.Core.Tests.csproj +++ b/Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj @@ -7,13 +7,14 @@ - - - + + + + diff --git a/Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs b/Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs new file mode 100644 index 00000000..93fc2a60 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs @@ -0,0 +1,246 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Client.Receiving; +using MQTTnet.Client.Subscribing; +using MQTTnet.Client.Unsubscribing; +using MQTTnet.Formatter; +using MQTTnet.Protocol; +using MQTTnet.Server; + +namespace MQTTnet.Tests.MQTTv5 +{ + [TestClass] + public class Client_Tests + { + [TestMethod] + public async Task Connect() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Connect_And_Disconnect() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + await client.DisconnectAsync(); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Subscribe() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + var result = await client.SubscribeAsync("a", MqttQualityOfServiceLevel.AtLeastOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(1, result.Items.Count); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS1, result.Items[0].ResultCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Unsubscribe() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + await client.SubscribeAsync("a"); + var result = await client.UnsubscribeAsync("a"); + await client.DisconnectAsync(); + + Assert.AreEqual(1, result.Items.Count); + Assert.AreEqual(MqttClientUnsubscribeResultCode.Success, result.Items[0].ReasonCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Publish_QoS_0() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + var result = await client.PublishAsync("a", "b"); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Publish_QoS_1() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + var result = await client.PublishAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Publish_QoS_2() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + var result = await client.PublishAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Publish_With_Properties() + { + var server = new MqttFactory().CreateMqttServer(); + var client = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("Hello") + .WithPayload("World") + .WithAtMostOnceQoS() + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithResponseTopic("response") + .WithContentType("text") + .WithMessageExpiryInterval(50) + .WithCorrelationData(new byte[12]) + .WithTopicAlias(2) + .Build(); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + var result = await client.PublishAsync(applicationMessage); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + finally + { + await server.StopAsync(); + } + } + + [TestMethod] + public async Task Subscribe_And_Publish() + { + var server = new MqttFactory().CreateMqttServer(); + var client1 = new MqttFactory().CreateMqttClient(); + var client2 = new MqttFactory().CreateMqttClient(); + + try + { + await server.StartAsync(new MqttServerOptions()); + + var receivedMessages = new List(); + + await client1.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithClientId("client1").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + client1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => + { + lock (receivedMessages) + { + receivedMessages.Add(e); + } + }); + + await client1.SubscribeAsync("a"); + + await client2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithClientId("client2").WithProtocolVersion(MqttProtocolVersion.V500).Build()); + await client2.PublishAsync("a", "b"); + + await Task.Delay(500); + + await client2.DisconnectAsync(); + await client1.DisconnectAsync(); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.AreEqual("client1", receivedMessages[0].ClientId); + Assert.AreEqual("a", receivedMessages[0].ApplicationMessage.Topic); + Assert.AreEqual("b", receivedMessages[0].ApplicationMessage.ConvertPayloadToString()); + } + finally + { + await server.StopAsync(); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MQTTv5/Feature_Tests.cs b/Tests/MQTTnet.Core.Tests/MQTTv5/Feature_Tests.cs new file mode 100644 index 00000000..294ee846 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MQTTv5/Feature_Tests.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet.Client.Subscribing; +using MQTTnet.Formatter; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests.MQTTv5 +{ + [TestClass] + public class Feature_Tests + { + [TestMethod] + public async Task Use_User_Properties() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var sender = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + var receiver = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + var message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithUserProperty("z", "3") + .WithUserProperty("z", "4"); // z is here two times to test list of items + + await receiver.SubscribeAsync(new MqttClientSubscribeOptions + { + TopicFilters = new List + { + new TopicFilter { Topic = "#" } + } + }, CancellationToken.None); + + MqttApplicationMessage receivedMessage = null; + receiver.UseApplicationMessageReceivedHandler(e => receivedMessage = e.ApplicationMessage); + + await sender.PublishAsync(message.Build(), CancellationToken.None); + + await Task.Delay(1000); + + Assert.IsNotNull(receivedMessage); + Assert.AreEqual("A", receivedMessage.Topic); + Assert.AreEqual(4, receivedMessage.UserProperties.Count); + + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs b/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs deleted file mode 100644 index 670b8a1c..00000000 --- a/Tests/MQTTnet.Core.Tests/ManagedMqttClientTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MQTTnet.Extensions.ManagedClient; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Server; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class ManagedMqttClientTests - { - [TestMethod] - public async Task Drop_New_Messages_On_Full_Queue() - { - var factory = new MqttFactory(); - var managedClient = factory.CreateManagedMqttClient(); - - var clientOptions = new ManagedMqttClientOptionsBuilder() - .WithMaxPendingMessages(5) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); - - clientOptions.WithClientOptions(o => o.WithTcpServer("localhost")); - - await managedClient.StartAsync(clientOptions.Build()); - - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "1" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "2" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "3" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "4" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "5" }); - - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "6" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "7" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "8" }); - - Assert.AreEqual(5, managedClient.PendingApplicationMessagesCount); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/ManagedMqttClient_Tests.cs b/Tests/MQTTnet.Core.Tests/ManagedMqttClient_Tests.cs new file mode 100644 index 00000000..ca665a1e --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/ManagedMqttClient_Tests.cs @@ -0,0 +1,112 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Options; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests +{ + [TestClass] + public class ManagedMqttClient_Tests + { + [TestMethod] + public async Task Drop_New_Messages_On_Full_Queue() + { + var factory = new MqttFactory(); + var managedClient = factory.CreateManagedMqttClient(); + try + { + var clientOptions = new ManagedMqttClientOptionsBuilder() + .WithMaxPendingMessages(5) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); + + clientOptions.WithClientOptions(o => o.WithTcpServer("localhost")); + + await managedClient.StartAsync(clientOptions.Build()); + + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "1" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "2" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "3" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "4" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "5" }); + + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "6" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "7" }); + await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "8" }); + + Assert.AreEqual(5, managedClient.PendingApplicationMessagesCount); + } + finally + { + await managedClient.StopAsync(); + } + } + + [TestMethod] + public async Task ManagedClients_Will_Message_Send() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + var factory = new MqttFactory(); + + await testEnvironment.StartServerAsync(); + + var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost", testEnvironment.ServerPort) + .WithWillMessage(willMessage); + var dyingClient = testEnvironment.CreateClient(); + var dyingManagedClient = new ManagedMqttClient(dyingClient, testEnvironment.ClientLogger.CreateChildLogger()); + await dyingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder() + .WithClientOptions(clientOptions) + .Build()); + + var recievingClient = await testEnvironment.ConnectClientAsync(); + await recievingClient.SubscribeAsync("My/last/will"); + + recievingClient.UseApplicationMessageReceivedHandler(context => Interlocked.Increment(ref receivedMessagesCount)); + + dyingManagedClient.Dispose(); + + await Task.Delay(1000); + + Assert.AreEqual(1, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Start_Stop() + { + using (var testEnvironment = new TestEnvironment()) + { + var factory = new MqttFactory(); + + var server = await testEnvironment.StartServerAsync(); + + var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetLogger().CreateChildLogger()); + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost", testEnvironment.ServerPort); + + TaskCompletionSource connected = new TaskCompletionSource(); + managedClient.ConnectedHandler = new MqttClientConnectedHandlerDelegate(e => { connected.SetResult(true);}); + + await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder() + .WithClientOptions(clientOptions) + .Build()); + + await connected.Task; + + await managedClient.StopAsync(); + + Assert.AreEqual(0, (await server.GetClientStatusAsync()).Count); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs new file mode 100644 index 00000000..776857bf --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet.Diagnostics; +using MQTTnet.Server; + +namespace MQTTnet.Tests.Mockups +{ + public class TestEnvironment : IDisposable + { + private readonly MqttFactory _mqttFactory = new MqttFactory(); + private readonly List _clients = new List(); + private readonly IMqttNetLogger _serverLogger = new MqttNetLogger("server"); + private readonly IMqttNetLogger _clientLogger = new MqttNetLogger("client"); + + private readonly List _serverErrors = new List(); + private readonly List _clientErrors = new List(); + + private readonly List _exceptions = new List(); + + public IMqttServer Server { get; private set; } + + public bool IgnoreClientLogErrors { get; set; } + + public bool IgnoreServerLogErrors { get; set; } + + public int ServerPort { get; set; } = 1888; + + public IMqttNetLogger ServerLogger => _serverLogger; + + public IMqttNetLogger ClientLogger => _clientLogger; + + public TestEnvironment() + { + _serverLogger.LogMessagePublished += (s, e) => + { + if (e.TraceMessage.Level == MqttNetLogLevel.Error) + { + lock (_serverErrors) + { + _serverErrors.Add(e.TraceMessage.ToString()); + } + } + }; + + _clientLogger.LogMessagePublished += (s, e) => + { + lock (_clientErrors) + { + if (e.TraceMessage.Level == MqttNetLogLevel.Error) + { + _clientErrors.Add(e.TraceMessage.ToString()); + } + } + }; + } + + public IMqttClient CreateClient() + { + var client = _mqttFactory.CreateMqttClient(_clientLogger); + _clients.Add(client); + return client; + } + + public Task StartServerAsync() + { + return StartServerAsync(new MqttServerOptionsBuilder()); + } + + public async Task StartServerAsync(MqttServerOptionsBuilder options) + { + if (Server != null) + { + throw new InvalidOperationException("Server already started."); + } + + Server = _mqttFactory.CreateMqttServer(_serverLogger); + await Server.StartAsync(options.WithDefaultEndpointPort(ServerPort).Build()); + + return Server; + } + + public Task ConnectClientAsync() + { + return ConnectClientAsync(new MqttClientOptionsBuilder()); + } + + public async Task ConnectClientAsync(MqttClientOptionsBuilder options) + { + var client = CreateClient(); + await client.ConnectAsync(options.WithTcpServer("localhost", ServerPort).Build()); + + return client; + } + + public void ThrowIfLogErrors() + { + lock (_serverErrors) + { + if (!IgnoreServerLogErrors && _serverErrors.Count > 0) + { + throw new Exception($"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."); + } + } + + lock (_clientErrors) + { + if (!IgnoreClientLogErrors && _clientErrors.Count > 0) + { + throw new Exception($"Client(s) had {_clientErrors.Count} errors (${string.Join(Environment.NewLine, _clientErrors)})."); + } + } + } + + public void Dispose() + { + foreach (var mqttClient in _clients) + { + mqttClient?.Dispose(); + } + + Server?.StopAsync().GetAwaiter().GetResult(); + + ThrowIfLogErrors(); + + if (_exceptions.Any()) + { + throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions)); + } + } + + public void TrackException(Exception exception) + { + lock (_exceptions) + { + _exceptions.Add(exception); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs similarity index 57% rename from Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs rename to Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs index 9375f07d..a58a77f6 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapter.cs +++ b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs @@ -3,10 +3,10 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; +using MQTTnet.Formatter; using MQTTnet.Packets; -using MQTTnet.Serializer; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests.Mockups { public class TestMqttCommunicationAdapter : IMqttChannelAdapter { @@ -14,12 +14,17 @@ namespace MQTTnet.Core.Tests public TestMqttCommunicationAdapter Partner { get; set; } - public string Endpoint { get; } + public string Endpoint { get; } = string.Empty; - public IMqttPacketSerializer PacketSerializer { get; } = new MqttPacketSerializer(); + public bool IsSecureConnection { get; } = false; - public event EventHandler ReadingPacketStarted; - public event EventHandler ReadingPacketCompleted; + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + + public long BytesSent { get; } + public long BytesReceived { get; } + + public Action ReadingPacketStartedCallback { get; set; } + public Action ReadingPacketCompletedCallback { get; set; } public void Dispose() { @@ -35,7 +40,7 @@ namespace MQTTnet.Core.Tests return Task.FromResult(0); } - public Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public Task SendPacketAsync(MqttBasePacket packet, TimeSpan timeout, CancellationToken cancellationToken) { ThrowIfPartnerIsNull(); @@ -44,30 +49,11 @@ namespace MQTTnet.Core.Tests return Task.FromResult(0); } - public async Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) + public Task ReceivePacketAsync(TimeSpan timeout, CancellationToken cancellationToken) { ThrowIfPartnerIsNull(); - - if (timeout > TimeSpan.Zero) - { - using (var timeoutCts = new CancellationTokenSource(timeout)) - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) - { - return await Task.Run(() => - { - try - { - return _incomingPackets.Take(cts.Token); - } - catch - { - return null; - } - }, cts.Token); - } - } - - return await Task.Run(() => + + return Task.Run(() => { try { diff --git a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs similarity index 89% rename from Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs rename to Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs index 6873c69e..ff47f710 100644 --- a/Tests/MQTTnet.Core.Tests/TestMqttCommunicationAdapterFactory.cs +++ b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs @@ -1,8 +1,8 @@ using MQTTnet.Adapter; -using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Diagnostics; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests.Mockups { public class TestMqttCommunicationAdapterFactory : IMqttClientAdapterFactory { diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs new file mode 100644 index 00000000..55ca6bb1 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MQTTnet.Server; + +namespace MQTTnet.Tests.Mockups +{ + public class TestServerStorage : IMqttServerStorage + { + public IList Messages = new List(); + + public Task SaveRetainedMessagesAsync(IList messages) + { + Messages = messages; + return Task.CompletedTask; + } + + public Task> LoadRetainedMessagesAsync() + { + return Task.FromResult(Messages); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs b/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs similarity index 96% rename from Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs rename to Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs index 59afcd68..f4e5d9fa 100644 --- a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilderTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs @@ -4,10 +4,10 @@ using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Protocol; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class MqttApplicationMessageBuilderTests + public class MqttApplicationMessageBuilder_Tests { [TestMethod] public void CreateApplicationMessage_TopicOnly() diff --git a/Tests/MQTTnet.Core.Tests/MqttClientTests.cs b/Tests/MQTTnet.Core.Tests/MqttClientTests.cs deleted file mode 100644 index 181a3142..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttClientTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Adapter; -using MQTTnet.Client; -using MQTTnet.Diagnostics; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Server; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class MqttClientTests - { - [TestMethod] - public async Task Client_Disconnect_Exception() - { - var factory = new MqttFactory(); - var client = factory.CreateMqttClient(); - - Exception ex = null; - client.Disconnected += (s, e) => - { - ex = e.Exception; - }; - - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); - } - catch - { - } - - Assert.IsNotNull(ex); - Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); - Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); - } - - [TestMethod] - public async Task Client_Publish() - { - var server = new MqttFactory().CreateMqttServer(); - - try - { - var receivedMessages = new List(); - - await server.StartAsync(new MqttServerOptions()); - - var client1 = new MqttFactory().CreateMqttClient(); - client1.ApplicationMessageReceived += (_, e) => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - }; - - await client1.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - await client1.SubscribeAsync("a"); - - var client2 = new MqttFactory().CreateMqttClient(); - await client2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); - await client2.PublishAsync(message); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessages.Count); - Assert.IsFalse(receivedMessages.First().Retain); // Must be false even if set above! - } - finally - { - await server.StopAsync(); - } - } - - [TestMethod] - public async Task Publish_Special_Content() - { - var factory = new MqttFactory(); - var server = factory.CreateMqttServer(); - var serverOptions = new MqttServerOptionsBuilder().Build(); - - var receivedMessages = new List(); - - var client = factory.CreateMqttClient(); - - try - { - await server.StartAsync(serverOptions); - - client.Connected += async (s, e) => - { - await client.SubscribeAsync("RCU/P1/H0001/R0003"); - - var msg = new MqttApplicationMessageBuilder() - .WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") - .WithTopic("RCU/P1/H0001/R0003"); - - await client.PublishAsync(msg.Build()); - }; - - client.ApplicationMessageReceived += (s, e) => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - }; - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessages.Count); - Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); - } - finally - { - await server.StopAsync(); - } - } - -#if DEBUG - [TestMethod] - public async Task Client_Cleanup_On_Authentification_Fails() - { - var channel = new TestMqttCommunicationAdapter(); - var channel2 = new TestMqttCommunicationAdapter(); - channel.Partner = channel2; - channel2.Partner = channel; - - Task.Run(async () => { - var connect = await channel2.ReceivePacketAsync(TimeSpan.Zero, CancellationToken.None); - await channel2.SendPacketAsync(new MqttConnAckPacket - { - ConnectReturnCode = Protocol.MqttConnectReturnCode.ConnectionRefusedNotAuthorized - }, CancellationToken.None); - }); - - var fake = new TestMqttCommunicationAdapterFactory(channel); - - var client = new MqttClient(fake, new MqttNetLogger()); - - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("any-server").Build()); - } - catch (Exception ex) - { - Assert.IsInstanceOfType(ex, typeof(MqttConnectingFailedException)); - } - - Assert.IsTrue(client._packetReceiverTask == null || client._packetReceiverTask.IsCompleted, "receive loop not completed"); - Assert.IsTrue(client._keepAliveMessageSenderTask == null || client._keepAliveMessageSenderTask.IsCompleted, "keepalive loop not completed"); - } -#endif - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttClient_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttClient_Tests.cs new file mode 100644 index 00000000..f146d17e --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttClient_Tests.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Subscribing; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests +{ + [TestClass] + public class Client_Tests + { + [TestMethod] + public async Task PacketIdentifier_In_Publish_Result() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + var client = await testEnvironment.ConnectClientAsync(); + + var result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.AtMostOnce); + Assert.AreEqual(null, result.PacketIdentifier); + + result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.AtMostOnce); + Assert.AreEqual(null, result.PacketIdentifier); + + result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)1, result.PacketIdentifier); + + result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)2, result.PacketIdentifier); + + result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)3, result.PacketIdentifier); + + result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)4, result.PacketIdentifier); + } + } + + [TestMethod] + public async Task Invalid_Connect_Throws_Exception() + { + var factory = new MqttFactory(); + using (var client = factory.CreateMqttClient()) + { + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); + + Assert.Fail("Must fail!"); + } + catch (Exception exception) + { + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); + Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); + } + } + } + + [TestMethod] + public async Task Disconnect_Event_Contains_Exception() + { + var factory = new MqttFactory(); + using (var client = factory.CreateMqttClient()) + { + Exception ex = null; + client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(e => + { + ex = e.Exception; + }); + + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); + } + catch + { + } + + Assert.IsNotNull(ex); + Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); + Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); + } + } + + [TestMethod] + public async Task Preserve_Message_Order() + { + // The messages are sent in reverse or to ensure that the delay in the handler + // needs longer for the first messages and later messages may be processed earlier (if there + // is an issue). + const int MessagesCount = 50; + + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var client1 = await testEnvironment.ConnectClientAsync(); + await client1.SubscribeAsync("x"); + + var receivedValues = new List(); + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + var value = int.Parse(eventArgs.ApplicationMessage.ConvertPayloadToString()); + await Task.Delay(value); + + lock (receivedValues) + { + receivedValues.Add(value); + } + } + + client1.UseApplicationMessageReceivedHandler(Handler1); + + var client2 = await testEnvironment.ConnectClientAsync(); + for (var i = MessagesCount; i > 0; i--) + { + await client2.PublishAsync("x", i.ToString()); + } + + await Task.Delay(5000); + + for (var i = MessagesCount; i > 0; i--) + { + Assert.AreEqual(i, receivedValues[MessagesCount - i]); + } + } + } + + [TestMethod] + public async Task Send_Reply_For_Any_Received_Message() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var client1 = await testEnvironment.ConnectClientAsync(); + await client1.SubscribeAsync("request/+"); + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + await client1.PublishAsync($"reply/{eventArgs.ApplicationMessage.Topic}"); + } + + client1.UseApplicationMessageReceivedHandler(Handler1); + + var client2 = await testEnvironment.ConnectClientAsync(); + await client2.SubscribeAsync("reply/#"); + + var replies = new List(); + + void Handler2(MqttApplicationMessageReceivedEventArgs eventArgs) + { + lock (replies) + { + replies.Add(eventArgs.ApplicationMessage.Topic); + } + } + + client2.UseApplicationMessageReceivedHandler((Action)Handler2); + + await Task.Delay(500); + + await client2.PublishAsync("request/a"); + await client2.PublishAsync("request/b"); + await client2.PublishAsync("request/c"); + + await Task.Delay(500); + + Assert.AreEqual("reply/request/a,reply/request/b,reply/request/c", string.Join(",", replies)); + } + } + + [TestMethod] + public async Task Publish_With_Correct_Retain_Flag() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var receivedMessages = new List(); + + var client1 = await testEnvironment.ConnectClientAsync(); + client1.UseApplicationMessageReceivedHandler(c => + { + lock (receivedMessages) + { + receivedMessages.Add(c.ApplicationMessage); + } + }); + + await client1.SubscribeAsync("a"); + + var client2 = await testEnvironment.ConnectClientAsync(); + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); + await client2.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.IsFalse(receivedMessages.First().Retain); // Must be false even if set above! + } + } + + [TestMethod] + public async Task Subscribe_In_Callback_Events() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var receivedMessages = new List(); + + var client = testEnvironment.CreateClient(); + + client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => + { + await client.SubscribeAsync("RCU/P1/H0001/R0003"); + + var msg = new MqttApplicationMessageBuilder() + .WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") + .WithTopic("RCU/P1/H0001/R0003"); + + await client.PublishAsync(msg.Build()); + }); + + client.UseApplicationMessageReceivedHandler(c => + { + lock (receivedMessages) + { + receivedMessages.Add(c.ApplicationMessage); + } + }); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); + } + } + + [TestMethod] + public async Task Message_Send_Retry() + { + using (var testEnvironment = new TestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + testEnvironment.IgnoreServerLogErrors = true; + + await testEnvironment.StartServerAsync( + new MqttServerOptionsBuilder() + .WithPersistentSessions() + .WithDefaultCommunicationTimeout(TimeSpan.FromMilliseconds(250))); + + var client1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithCleanSession(false)); + await client1.SubscribeAsync("x", MqttQualityOfServiceLevel.AtLeastOnce); + + var retries = 0; + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + retries++; + + await Task.Delay(1000); + throw new Exception("Broken!"); + } + + client1.UseApplicationMessageReceivedHandler(Handler1); + + var client2 = await testEnvironment.ConnectClientAsync(); + await client2.PublishAsync("x"); + + await Task.Delay(3000); + + // The server should disconnect clients which are not responding. + Assert.IsFalse(client1.IsConnected); + + await client1.ReconnectAsync().ConfigureAwait(false); + + await Task.Delay(1000); + + Assert.AreEqual(2, retries); + } + } + + [TestMethod] + public async Task NoConnectedHandler_Connect_DoesNotThrowException() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var client = await testEnvironment.ConnectClientAsync(); + + Assert.IsTrue(client.IsConnected); + } + } + + [TestMethod] + public async Task NoDisconnectedHandler_Disconnect_DoesNotThrowException() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + var client = await testEnvironment.ConnectClientAsync(); + Assert.IsTrue(client.IsConnected); + + await client.DisconnectAsync(); + + Assert.IsFalse(client.IsConnected); + } + } + + [TestMethod] + public async Task Frequent_Connects() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var clients = new List(); + for (var i = 0; i < 100; i++) + { + clients.Add(await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("a"))); + } + + var clientStatus = await testEnvironment.Server.GetClientStatusAsync(); + var sessionStatus = await testEnvironment.Server.GetSessionStatusAsync(); + + for (var i = 0; i < 98; i++) + { + Assert.IsFalse(clients[i].IsConnected); + } + + Assert.IsTrue(clients[99].IsConnected); + + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(1, sessionStatus.Count); + + var receiveClient = clients[99]; + object receivedPayload = null; + receiveClient.UseApplicationMessageReceivedHandler(e => + { + receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); + }); + + await receiveClient.SubscribeAsync("x"); + + var sendClient = await testEnvironment.ConnectClientAsync(); + await sendClient.PublishAsync("x", "1"); + + await Task.Delay(100); + + Assert.AreEqual("1", receivedPayload); + } + } + + [TestMethod] + public async Task No_Payload() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var sender = await testEnvironment.ConnectClientAsync(); + var receiver = await testEnvironment.ConnectClientAsync(); + + var message = new MqttApplicationMessageBuilder() + .WithTopic("A"); + + await receiver.SubscribeAsync(new MqttClientSubscribeOptions + { + TopicFilters = new List { new TopicFilter { Topic = "#" } } + }, CancellationToken.None); + + MqttApplicationMessage receivedMessage = null; + receiver.UseApplicationMessageReceivedHandler(e => receivedMessage = e.ApplicationMessage); + + await sender.PublishAsync(message.Build(), CancellationToken.None); + + await Task.Delay(1000); + + Assert.IsNotNull(receivedMessage); + Assert.AreEqual("A", receivedMessage.Topic); + Assert.AreEqual(null, receivedMessage.Payload); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttFactory_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttFactory_Tests.cs new file mode 100644 index 00000000..75bda9bd --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttFactory_Tests.cs @@ -0,0 +1,77 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttFactory_Tests + { + [TestMethod] + public async Task Create_Managed_Client_With_Logger() + { + var factory = new MqttFactory(); + + //This test compares + //1. correct logID + string logId = "logId"; + bool invalidLogIdOccured = false; + + //2. if the total log calls are the same for global and local + int globalLogCount = 0; + int localLogCount = 0; + + MqttNetLogger logger = new MqttNetLogger(logId); + + //we have a theoretical bug here if a concurrent test is also logging + var globalLog = new EventHandler((s, e) => + { + if (logId != e.TraceMessage.LogId) + { + invalidLogIdOccured = true; + } + Interlocked.Increment(ref globalLogCount); + }); + + MqttNetGlobalLogger.LogMessagePublished += globalLog; + + logger.LogMessagePublished += (s, e) => + { + if (logId != e.TraceMessage.LogId) + { + invalidLogIdOccured = true; + } + Interlocked.Increment(ref localLogCount); + }; + + var managedClient = factory.CreateManagedMqttClient(logger); + try + { + var clientOptions = new ManagedMqttClientOptionsBuilder(); + + clientOptions.WithClientOptions(o => o.WithTcpServer("this_is_an_invalid_host").WithCommunicationTimeout(TimeSpan.FromSeconds(1))); + + //try connect to get some log entries + await managedClient.StartAsync(clientOptions.Build()); + + //wait at least connect timeout or we have some log messages + var tcs = new TaskCompletionSource(); + managedClient.ConnectingFailedHandler = new ConnectingFailedHandlerDelegate(e => tcs.TrySetResult(null)); + await Task.WhenAny(Task.Delay(managedClient.Options.ClientOptions.CommunicationTimeout), tcs.Task); + } + finally + { + await managedClient.StopAsync(); + + MqttNetGlobalLogger.LogMessagePublished -= globalLog; + } + + Assert.IsFalse(invalidLogIdOccured); + Assert.AreNotEqual(0, globalLogCount); + Assert.AreEqual(globalLogCount, localLogCount); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitor_Tests.cs similarity index 82% rename from Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs rename to Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitor_Tests.cs index b18904a0..ea2ab0f2 100644 --- a/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitorTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttKeepAliveMonitor_Tests.cs @@ -5,13 +5,13 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Packets; using MQTTnet.Server; +using MQTTnet.Server.Status; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class MqttKeepAliveMonitorTests + public class MqttKeepAliveMonitor_Tests { [TestMethod] public void KeepAlive_Timeout() @@ -44,9 +44,9 @@ namespace MQTTnet.Core.Tests // Simulate traffic. Thread.Sleep(1000); // Internally the keep alive timeout is multiplied with 1.5 as per protocol specification. - monitor.PacketReceived(new MqttPublishPacket()); + monitor.PacketReceived(); Thread.Sleep(1000); - monitor.PacketReceived(new MqttPublishPacket()); + monitor.PacketReceived(); Thread.Sleep(1000); Assert.AreEqual(0, clientSession.StopCalledCount); @@ -62,12 +62,12 @@ namespace MQTTnet.Core.Tests public int StopCalledCount { get; private set; } - public void FillStatus(MqttClientSessionStatus status) + public void FillStatus(MqttClientStatus status) { throw new NotSupportedException(); } - public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttPublishPacket publishPacket) + public void EnqueueApplicationMessage(MqttClientConnection senderClientSession, MqttApplicationMessage applicationMessage) { throw new NotSupportedException(); } @@ -77,14 +77,15 @@ namespace MQTTnet.Core.Tests throw new NotSupportedException(); } - public Task RunAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter adapter) + public Task RunAsync(MqttApplicationMessage willMessage, int keepAlivePeriod, IMqttChannelAdapter adapter) { throw new NotSupportedException(); } - public void Stop(MqttClientDisconnectType disconnectType) + public Task StopAsync() { StopCalledCount++; + return Task.FromResult(0); } public Task SubscribeAsync(IList topicFilters) diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs deleted file mode 100644 index ab7070ee..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProviderTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.VisualStudio.TestTools .UnitTesting; -using MQTTnet.Client; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class MqttPacketIdentifierProviderTests - { - [TestMethod] - public void Reset() - { - var p = new MqttPacketIdentifierProvider(); - Assert.AreEqual(1, p.GetNewPacketIdentifier()); - Assert.AreEqual(2, p.GetNewPacketIdentifier()); - p.Reset(); - Assert.AreEqual(1, p.GetNewPacketIdentifier()); - } - - [TestMethod] - public void ReachBoundaries() - { - var p = new MqttPacketIdentifierProvider(); - - for (ushort i = 0; i < ushort.MaxValue; i++) - { - Assert.AreEqual(i + 1, p.GetNewPacketIdentifier()); - } - - Assert.AreEqual(1, p.GetNewPacketIdentifier()); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs new file mode 100644 index 00000000..d3fb35c2 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs @@ -0,0 +1,32 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttPacketIdentifierProvider_Tests + { + [TestMethod] + public void Reset() + { + var p = new MqttPacketIdentifierProvider(); + Assert.AreEqual(1, p.GetNextPacketIdentifier()); + Assert.AreEqual(2, p.GetNextPacketIdentifier()); + p.Reset(); + Assert.AreEqual(1, p.GetNextPacketIdentifier()); + } + + [TestMethod] + public void ReachBoundaries() + { + var p = new MqttPacketIdentifierProvider(); + + for (ushort i = 0; i < ushort.MaxValue; i++) + { + Assert.AreEqual(i + 1, p.GetNextPacketIdentifier()); + } + + Assert.AreEqual(1, p.GetNextPacketIdentifier()); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs deleted file mode 100644 index 39aeeff2..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttPacketReaderTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.IO; -using System.Threading; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Exceptions; -using MQTTnet.Internal; -using MQTTnet.Serializer; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class MqttPacketReaderTests - { - [TestMethod] - [ExpectedException(typeof(MqttCommunicationClosedGracefullyException))] - public void MqttPacketReader_EmptyStream() - { - var fixedHeader = new byte[2]; - var singleByteBuffer = new byte[1]; - MqttPacketReader.ReadFixedHeaderAsync(new TestMqttChannel(new MemoryStream()), fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs new file mode 100644 index 00000000..c073cfc6 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; +using MQTTnet.Internal; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttPacketReader_Tests + { + [TestMethod] + public async Task MqttPacketReader_EmptyStream() + { + var fixedHeader = new byte[2]; + var reader = new MqttPacketReader(new TestMqttChannel(new MemoryStream())); + var readResult = await reader.ReadFixedHeaderAsync(fixedHeader, CancellationToken.None); + + Assert.IsTrue(readResult.ConnectionClosed); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs similarity index 73% rename from Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs rename to Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs index c3a75f8e..429a2312 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketSerializerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs @@ -5,15 +5,16 @@ using System.Text; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; +using MQTTnet.Formatter; +using MQTTnet.Formatter.V3; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -using MQTTnet.Serializer; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class MqttPacketSerializerTests + public class MqttPacketSerializer_Tests { [TestMethod] public void SerializeV310_MqttConnectPacket() @@ -110,7 +111,7 @@ namespace MQTTnet.Core.Tests var p = new MqttConnAckPacket { IsSessionPresent = true, - ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; SerializeAndCompare(p, "IAIBBQ=="); @@ -121,7 +122,7 @@ namespace MQTTnet.Core.Tests { var p = new MqttConnAckPacket { - ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); @@ -133,7 +134,7 @@ namespace MQTTnet.Core.Tests var p = new MqttConnAckPacket { IsSessionPresent = true, - ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; DeserializeAndCompare(p, "IAIBBQ=="); @@ -144,7 +145,7 @@ namespace MQTTnet.Core.Tests { var p = new MqttConnAckPacket { - ConnectReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); @@ -153,7 +154,7 @@ namespace MQTTnet.Core.Tests [TestMethod] public void Serialize_LargePacket() { - var serializer = new MqttPacketSerializer { ProtocolVersion = MqttProtocolVersion.V311 }; + var serializer = new MqttV311PacketFormatter(); const int payloadLength = 80000; @@ -176,22 +177,21 @@ namespace MQTTnet.Core.Tests Payload = payload }; - var buffer = serializer.Serialize(publishPacket); + var buffer = serializer.Encode(publishPacket); var testChannel = new TestMqttChannel(new MemoryStream(buffer.Array, buffer.Offset, buffer.Count)); - var header = MqttPacketReader.ReadFixedHeaderAsync( - testChannel, + var header = new MqttPacketReader(testChannel).ReadFixedHeaderAsync( new byte[2], - new byte[1], - CancellationToken.None).GetAwaiter().GetResult(); + CancellationToken.None).GetAwaiter().GetResult().FixedHeader; var eof = buffer.Offset + buffer.Count; var receivedPacket = new ReceivedMqttPacket( header.Flags, - new MqttPacketBodyReader(buffer.Array, eof - header.RemainingLength, buffer.Count + buffer.Offset)); + new MqttPacketBodyReader(buffer.Array, eof - header.RemainingLength, buffer.Count + buffer.Offset), + 0); - var packet = (MqttPublishPacket)serializer.Deserialize(receivedPacket); + var packet = (MqttPublishPacket)serializer.Decode(receivedPacket); Assert.AreEqual(publishPacket.Topic, packet.Topic); Assert.IsTrue(publishPacket.Payload.SequenceEqual(packet.Payload)); @@ -231,6 +231,32 @@ namespace MQTTnet.Core.Tests SerializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); } + [TestMethod] + public void SerializeV500_MqttPublishPacket() + { + var prop = new MqttPublishPacketProperties(); + + prop.ResponseTopic = "/Response"; + + prop.UserProperties.Add(new MqttUserProperty("Foo", "Bar")); + + var p = new MqttPublishPacket + { + PacketIdentifier = 123, + Dup = true, + Retain = true, + Payload = Encoding.ASCII.GetBytes("HELLO"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "A/B/C", + Properties = prop + }; + + var deserialized = Roundtrip(p, MqttProtocolVersion.V500); + + Assert.AreEqual(prop.ResponseTopic, deserialized.Properties.ResponseTopic); + Assert.IsTrue(deserialized.Properties.UserProperties.Any(x => x.Name == "Foo")); + } + [TestMethod] public void DeserializeV311_MqttPublishPacket() { @@ -401,9 +427,9 @@ namespace MQTTnet.Core.Tests PacketIdentifier = 123 }; - p.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce)); - p.TopicFilters.Add(new TopicFilter("1/2/3", MqttQualityOfServiceLevel.AtLeastOnce)); - p.TopicFilters.Add(new TopicFilter("x/y/z", MqttQualityOfServiceLevel.AtMostOnce)); + p.TopicFilters.Add(new TopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + p.TopicFilters.Add(new TopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + p.TopicFilters.Add(new TopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); SerializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); } @@ -416,9 +442,9 @@ namespace MQTTnet.Core.Tests PacketIdentifier = 123 }; - p.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce)); - p.TopicFilters.Add(new TopicFilter("1/2/3", MqttQualityOfServiceLevel.AtLeastOnce)); - p.TopicFilters.Add(new TopicFilter("x/y/z", MqttQualityOfServiceLevel.AtMostOnce)); + p.TopicFilters.Add(new TopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + p.TopicFilters.Add(new TopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + p.TopicFilters.Add(new TopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); DeserializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); } @@ -431,10 +457,10 @@ namespace MQTTnet.Core.Tests PacketIdentifier = 123 }; - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); + p.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); SerializeAndCompare(p, "kAYAewABAoA="); } @@ -447,10 +473,10 @@ namespace MQTTnet.Core.Tests PacketIdentifier = 123 }; - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); - p.SubscribeReturnCodes.Add(MqttSubscribeReturnCode.Failure); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); + p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); + p.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); DeserializeAndCompare(p, "kAYAewABAoA="); } @@ -509,53 +535,80 @@ namespace MQTTnet.Core.Tests private static void SerializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { - var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; - var data = serializer.Serialize(packet); + IMqttPacketFormatter serializer; + if (protocolVersion == MqttProtocolVersion.V311) + { + serializer = new MqttV311PacketFormatter(); + } + else if (protocolVersion == MqttProtocolVersion.V310) + { + serializer = new MqttV310PacketFormatter(); + } + else + { + throw new NotSupportedException(); + } + + var data = serializer.Encode(packet); Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(data))); } - private static void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + protected virtual IMqttPacketWriter WriterFactory() { - var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; + return new MqttPacketWriter(); + } - var buffer1 = serializer.Serialize(packet); + protected virtual IMqttPacketBodyReader ReaderFactory(byte[] data) + { + return new MqttPacketBodyReader(data, 0, data.Length); + } + + private void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + { + var writer = WriterFactory(); + + var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); + + var buffer1 = serializer.Encode(packet); using (var headerStream = new MemoryStream(Join(buffer1))) { var channel = new TestMqttChannel(headerStream); var fixedHeader = new byte[2]; - var singleByteBuffer = new byte[1]; - var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + var header = new MqttPacketReader(channel).ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.RemainingLength)) { - var deserializedPacket = serializer.Deserialize(new ReceivedMqttPacket(header.Flags, new MqttPacketBodyReader(bodyStream.ToArray(), 0, (int)bodyStream.Length))); - var buffer2 = serializer.Serialize(deserializedPacket); + var reader = ReaderFactory(bodyStream.ToArray()); + var deserializedPacket = serializer.Decode(new ReceivedMqttPacket(header.Flags, reader, 0)); + var buffer2 = serializer.Encode(deserializedPacket); Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(buffer2))); } } } - private static T Roundtrip(T packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + private T Roundtrip(T packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) where T : MqttBasePacket { - var serializer = new MqttPacketSerializer { ProtocolVersion = protocolVersion }; + var writer = WriterFactory(); - var buffer1 = serializer.Serialize(packet); + var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); + + var buffer1 = serializer.Encode(packet); using (var headerStream = new MemoryStream(Join(buffer1))) { var channel = new TestMqttChannel(headerStream); var fixedHeader = new byte[2]; - var singleByteBuffer = new byte[1]; - var header = MqttPacketReader.ReadFixedHeaderAsync(channel, fixedHeader, singleByteBuffer, CancellationToken.None).GetAwaiter().GetResult(); + var header = new MqttPacketReader(channel).ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; - using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.RemainingLength)) + using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, (int)header.RemainingLength)) { - return (T)serializer.Deserialize(new ReceivedMqttPacket(header.Flags, new MqttPacketBodyReader(bodyStream.ToArray(), 0, (int)bodyStream.Length))); + var reader = ReaderFactory(bodyStream.ToArray()); + return (T)serializer.Decode(new ReceivedMqttPacket(header.Flags, reader, 0)); } } } diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs similarity index 86% rename from Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs rename to Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs index 8b73b739..bd97c1ce 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketWriterTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs @@ -1,10 +1,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Serializer; +using MQTTnet.Formatter; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class MqttPacketWriterTests + public class MqttPacketWriter_Tests { [TestMethod] public void WritePacket() diff --git a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs b/Tests/MQTTnet.Core.Tests/MqttServerTests.cs deleted file mode 100644 index 457de926..00000000 --- a/Tests/MQTTnet.Core.Tests/MqttServerTests.cs +++ /dev/null @@ -1,874 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Diagnostics; -using MQTTnet.Protocol; -using MQTTnet.Server; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Implementations; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class MqttServerTests - { - [TestMethod] - public void MqttServer_PublishSimple_AtMostOnce() - { - TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.AtMostOnce, - "A/B/C", - MqttQualityOfServiceLevel.AtMostOnce, - 1).Wait(); - } - - [TestMethod] - public void MqttServer_PublishSimple_AtLeastOnce() - { - TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.AtLeastOnce, - "A/B/C", - MqttQualityOfServiceLevel.AtLeastOnce, - 1).Wait(); - } - - [TestMethod] - public void MqttServer_PublishSimple_ExactlyOnce() - { - TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.ExactlyOnce, - "A/B/C", - MqttQualityOfServiceLevel.ExactlyOnce, - 1).Wait(); - } - - [TestMethod] - public async Task MqttServer_WillMessage() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessagesCount = 0; - try - { - await s.StartAsync(new MqttServerOptions()); - - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); - var c1 = await serverAdapter.ConnectTestClient("c1"); - var c2 = await serverAdapter.ConnectTestClient("c2", willMessage); - - c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); - - await c2.DisconnectAsync(); - - await Task.Delay(1000); - - await c1.DisconnectAsync(); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(0, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_SubscribeUnsubscribe() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessagesCount = 0; - - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - var c2 = await serverAdapter.ConnectTestClient("c2"); - c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - - await c2.PublishAsync(message); - await Task.Delay(1000); - Assert.AreEqual(0, receivedMessagesCount); - - var subscribeEventCalled = false; - s.ClientSubscribedTopic += (_, e) => - { - subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == "c1"; - }; - - await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - await Task.Delay(500); - Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); - - await c2.PublishAsync(message); - await Task.Delay(500); - Assert.AreEqual(1, receivedMessagesCount); - - var unsubscribeEventCalled = false; - s.ClientUnsubscribedTopic += (_, e) => - { - unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == "c1"; - }; - - await c1.UnsubscribeAsync("a"); - await Task.Delay(500); - Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); - - await c2.PublishAsync(message); - await Task.Delay(1000); - Assert.AreEqual(1, receivedMessagesCount); - } - finally - { - await s.StopAsync(); - } - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_Publish() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var receivedMessagesCount = 0; - - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - - c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - - await s.PublishAsync(message); - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(1, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_Publish_MultipleClients() - { - var serverAdapter = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - var receivedMessagesCount = 0; - var locked = new object(); - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - var clientOptions2 = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = new MqttFactory().CreateMqttClient(); - var c2 = new MqttFactory().CreateMqttClient(); - - await c1.ConnectAsync(clientOptions); - await c2.ConnectAsync(clientOptions2); - - c1.ApplicationMessageReceived += (_, __) => - { - lock (locked) - { - receivedMessagesCount++; - } - }; - - c2.ApplicationMessageReceived += (_, __) => - { - lock (locked) - { - receivedMessagesCount++; - } - }; - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - await c1.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - await c2.SubscribeAsync(new TopicFilter("a", MqttQualityOfServiceLevel.AtLeastOnce)); - - //await Task.WhenAll(Publish(c1, message), Publish(c2, message)); - await Publish(c1, message); - - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(2000, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_SessionTakeover() - { - var server = new MqttFactory().CreateMqttServer(); - try - { - await server.StartAsync(new MqttServerOptions()); - - var client1 = new MqttFactory().CreateMqttClient(); - var client2 = new MqttFactory().CreateMqttClient(); - - var options = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .WithCleanSession(false) - .WithClientId("a").Build(); - - await client1.ConnectAsync(options); - - await Task.Delay(500); - - await client2.ConnectAsync(options); - - await Task.Delay(500); - - Assert.IsFalse(client1.IsConnected); - Assert.IsTrue(client2.IsConnected); - } - finally - { - await server.StopAsync(); - } - } - - private static async Task Publish(IMqttClient c1, MqttApplicationMessage message) - { - for (int i = 0; i < 1000; i++) - { - await c1.PublishAsync(message); - } - } - - [TestMethod] - public async Task MqttServer_ShutdownDisconnectsClientsGracefully() - { - var serverAdapter = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - - var disconnectCalled = 0; - - await s.StartAsync(new MqttServerOptions()); - - var c1 = new MqttFactory().CreateMqttClient(); - c1.Disconnected += (sender, args) => disconnectCalled++; - - await c1.ConnectAsync(clientOptions); - - await Task.Delay(100); - - await s.StopAsync(); - - await Task.Delay(100); - - Assert.AreEqual(1, disconnectCalled); - } - - [TestMethod] - public async Task MqttServer_HandleCleanDisconnect() - { - var serverAdapter = new MqttTcpServerAdapter(new MqttNetLogger().CreateChildLogger()); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var clientConnectedCalled = 0; - var clientDisconnectedCalled = 0; - - s.ClientConnected += (_, __) => clientConnectedCalled++; - s.ClientDisconnected += (_, __) => clientDisconnectedCalled++; - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - - await s.StartAsync(new MqttServerOptions()); - - var c1 = new MqttFactory().CreateMqttClient(); - - await c1.ConnectAsync(clientOptions); - - await Task.Delay(100); - - await c1.DisconnectAsync(); - - await Task.Delay(100); - - await s.StopAsync(); - - await Task.Delay(100); - - Assert.AreEqual(clientConnectedCalled, clientDisconnectedCalled); - } - - [TestMethod] - public async Task MqttServer_Client_Disconnect_Without_Errors() - { - var errors = 0; - - MqttNetGlobalLogger.LogMessagePublished += (_, e) => - { - System.Diagnostics.Debug.WriteLine($"[{e.TraceMessage.Timestamp:s}] {e.TraceMessage.Source} {e.TraceMessage.Message}"); - - if (e.TraceMessage.Level == MqttNetLogLevel.Error) - { - errors++; - } - }; - - bool clientWasConnected; - - var server = new MqttFactory().CreateMqttServer(); - try - { - var options = new MqttServerOptionsBuilder().Build(); - await server.StartAsync(options); - - var client = new MqttFactory().CreateMqttClient(); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - - await client.ConnectAsync(clientOptions); - - clientWasConnected = true; - - await client.DisconnectAsync(); - - await Task.Delay(500); - } - finally - { - await server.StopAsync(); - } - - Assert.IsTrue(clientWasConnected); - Assert.AreEqual(0, errors); - } - - [TestMethod] - public async Task MqttServer_LotsOfRetainedMessages() - { - const int ClientCount = 100; - - var server = new MqttFactory().CreateMqttServer(); - try - { - await server.StartAsync(new MqttServerOptionsBuilder().Build()); - - Parallel.For( - 0, - ClientCount, - new ParallelOptions { MaxDegreeOfParallelism = 10 }, - i => - { - using (var client = new MqttFactory().CreateMqttClient()) - { - client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()) - .GetAwaiter().GetResult(); - - for (var j = 0; j < 10; j++) - { - // Clear retained message. - client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i) - .WithPayload(new byte[0]).WithRetainFlag().Build()).GetAwaiter().GetResult(); - - // Set retained message. - client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i) - .WithPayload("value" + j).WithRetainFlag().Build()).GetAwaiter().GetResult(); - } - - client.DisconnectAsync().GetAwaiter().GetResult(); - } - }); - - await Task.Delay(100); - - var retainedMessages = server.GetRetainedMessages(); - - Assert.AreEqual(ClientCount, retainedMessages.Count); - - for (var i = 0; i < ClientCount; i++) - { - Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); - } - } - finally - { - await server.StopAsync(); - } - } - - [TestMethod] - public async Task MqttServer_RetainedMessagesFlow() - { - var retainedMessage = new MqttApplicationMessageBuilder().WithTopic("r").WithPayload("r").WithRetainFlag().Build(); - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - await s.StartAsync(new MqttServerOptions()); - var c1 = await serverAdapter.ConnectTestClient("c1"); - await c1.PublishAsync(retainedMessage); - Thread.Sleep(500); - await c1.DisconnectAsync(); - Thread.Sleep(500); - - var receivedMessages = 0; - var c2 = await serverAdapter.ConnectTestClient("c2"); - c2.ApplicationMessageReceived += (_, e) => - { - receivedMessages++; - }; - - for (var i = 0; i < 5; i++) - { - await c2.UnsubscribeAsync("r"); - await Task.Delay(500); - Assert.AreEqual(i, receivedMessages); - - await c2.SubscribeAsync("r"); - await Task.Delay(500); - Assert.AreEqual(i + 1, receivedMessages); - } - - await c2.DisconnectAsync(); - - await s.StopAsync(); - } - - [TestMethod] - public async Task MqttServer_NoRetainedMessage() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessagesCount = 0; - - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[3])); - await c1.DisconnectAsync(); - - var c2 = await serverAdapter.ConnectTestClient("c2"); - c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); - - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(0, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_RetainedMessage() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessages = new List(); - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c1.DisconnectAsync(); - - var c2 = await serverAdapter.ConnectTestClient("c2"); - c2.ApplicationMessageReceived += (_, e) => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - }; - - await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); - - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(1, receivedMessages.Count); - Assert.IsTrue(receivedMessages.First().Retain); - } - - [TestMethod] - public async Task MqttServer_ClearRetainedMessage() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessagesCount = 0; - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag()); - await c1.PublishAsync(builder => builder.WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag()); - await c1.DisconnectAsync(); - - var c2 = await serverAdapter.ConnectTestClient("c2"); - c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - - await Task.Delay(200); - await c2.SubscribeAsync(new TopicFilter("retained", MqttQualityOfServiceLevel.AtMostOnce)); - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - - Assert.AreEqual(0, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_PersistRetainedMessage() - { - var storage = new TestStorage(); - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - try - { - var options = new MqttServerOptions { Storage = storage }; - - await s.StartAsync(options); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - - await c1.PublishAndWaitForAsync(s, new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - - await Task.Delay(250); - - await c1.DisconnectAsync(); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(1, storage.Messages.Count); - - s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var receivedMessagesCount = 0; - try - { - var options = new MqttServerOptions { Storage = storage }; - await s.StartAsync(options); - - var c2 = await serverAdapter.ConnectTestClient("c2"); - c2.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); - - await Task.Delay(250); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(1, receivedMessagesCount); - } - - [TestMethod] - public async Task MqttServer_InterceptMessage() - { - void Interceptor(MqttApplicationMessageInterceptorContext context) - { - context.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); - } - - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - try - { - var options = new MqttServerOptions { ApplicationMessageInterceptor = Interceptor }; - - await s.StartAsync(options); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - var c2 = await serverAdapter.ConnectTestClient("c2"); - await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("test").Build()); - - var isIntercepted = false; - c2.ApplicationMessageReceived += (sender, args) => - { - isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(args.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; - }; - - await c1.PublishAsync(builder => builder.WithTopic("test")); - await c1.DisconnectAsync(); - - await Task.Delay(500); - - Assert.IsTrue(isIntercepted); - } - finally - { - await s.StopAsync(); - } - } - - [TestMethod] - public async Task MqttServer_Body() - { - var serverAdapter = new TestMqttServerAdapter(); - var s = new MqttFactory().CreateMqttServer(new[] { serverAdapter }, new MqttNetLogger()); - - var bodyIsMatching = false; - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = await serverAdapter.ConnectTestClient("c1"); - var c2 = await serverAdapter.ConnectTestClient("c2"); - - c1.ApplicationMessageReceived += (_, e) => - { - if (Encoding.UTF8.GetString(e.ApplicationMessage.Payload) == "The body") - { - bodyIsMatching = true; - } - }; - - await c1.SubscribeAsync("A", MqttQualityOfServiceLevel.AtMostOnce); - await c2.PublishAsync(builder => builder.WithTopic("A").WithPayload(Encoding.UTF8.GetBytes("The body"))); - - await Task.Delay(1000); - } - finally - { - await s.StopAsync(); - } - - Assert.IsTrue(bodyIsMatching); - } - - [TestMethod] - public async Task MqttServer_ConnectionDenied() - { - var server = new MqttFactory().CreateMqttServer(); - var client = new MqttFactory().CreateMqttClient(); - - try - { - var options = new MqttServerOptionsBuilder().WithConnectionValidator(context => - { - context.ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized; - }).Build(); - - await server.StartAsync(options); - - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost").Build(); - - try - { - await client.ConnectAsync(clientOptions); - Assert.Fail("An exception should be raised."); - } - catch (Exception exception) - { - if (exception is MqttConnectingFailedException) - { - - } - else - { - Assert.Fail("Wrong exception."); - } - } - } - finally - { - await client.DisconnectAsync(); - await server.StopAsync(); - - client.Dispose(); - } - } - - [TestMethod] - public async Task MqttServer_SameClientIdConnectDisconnectEventOrder() - { - var s = new MqttFactory().CreateMqttServer(); - - var events = new List(); - - s.ClientConnected += (_, __) => - { - lock (events) - { - events.Add("c"); - } - }; - - s.ClientDisconnected += (_, __) => - { - lock (events) - { - events.Add("d"); - } - }; - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .WithClientId("same_id") - .Build(); - - await s.StartAsync(new MqttServerOptions()); - - var c1 = new MqttFactory().CreateMqttClient(); - var c2 = new MqttFactory().CreateMqttClient(); - - await c1.ConnectAsync(clientOptions); - - await Task.Delay(250); - - await c2.ConnectAsync(clientOptions); - - await Task.Delay(250); - - await c1.DisconnectAsync(); - - await Task.Delay(250); - - await c2.DisconnectAsync(); - - await Task.Delay(250); - - await s.StopAsync(); - - var flow = string.Join(string.Empty, events); - Assert.AreEqual("cdcd", flow); - } - - - [TestMethod] - public async Task MqttServer_StopAndRestart() - { - var server = new MqttFactory().CreateMqttServer(); - await server.StartAsync(new MqttServerOptions()); - - var client = new MqttFactory().CreateMqttClient(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - await server.StopAsync(); - - try - { - var client2 = new MqttFactory().CreateMqttClient(); - await client2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - - Assert.Fail("Connecting should fail."); - } - catch (Exception) - { - } - - await server.StartAsync(new MqttServerOptions()); - var client3 = new MqttFactory().CreateMqttClient(); - await client3.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - - await server.StopAsync(); - } - - private class TestStorage : IMqttServerStorage - { - public IList Messages = new List(); - - public Task SaveRetainedMessagesAsync(IList messages) - { - Messages = messages; - return Task.CompletedTask; - } - - public Task> LoadRetainedMessagesAsync() - { - return Task.FromResult(Messages); - } - } - - private static async Task TestPublishAsync( - string topic, - MqttQualityOfServiceLevel qualityOfServiceLevel, - string topicFilter, - MqttQualityOfServiceLevel filterQualityOfServiceLevel, - int expectedReceivedMessagesCount) - { - var s = new MqttFactory().CreateMqttServer(); - - var receivedMessagesCount = 0; - try - { - await s.StartAsync(new MqttServerOptions()); - - var c1 = new MqttFactory().CreateMqttClient(); - c1.ApplicationMessageReceived += (_, __) => receivedMessagesCount++; - await c1.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); - - var c2 = new MqttFactory().CreateMqttClient(); - await c2.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - await c2.PublishAsync(builder => builder.WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel)); - - await Task.Delay(500); - await c1.UnsubscribeAsync(topicFilter); - await Task.Delay(500); - } - finally - { - await s.StopAsync(); - } - - Assert.AreEqual(expectedReceivedMessagesCount, receivedMessagesCount); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManager_Tests.cs similarity index 71% rename from Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs rename to Tests/MQTTnet.Core.Tests/MqttSubscriptionsManager_Tests.cs index 2bdd15f6..38fb99be 100644 --- a/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManagerTests.cs +++ b/Tests/MQTTnet.Core.Tests/MqttSubscriptionsManager_Tests.cs @@ -3,20 +3,20 @@ using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class MqttSubscriptionsManagerTests + public class MqttSubscriptionsManager_Tests { [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleSuccess() { - var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServerEventDispatcher()); + var sm = new MqttClientSubscriptionsManager("", new MqttServerEventDispatcher(), new MqttServerOptions()); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp); + sm.SubscribeAsync(sp).GetAwaiter().GetResult(); var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce); Assert.IsTrue(result.IsSubscribed); @@ -26,12 +26,12 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() { - var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServerEventDispatcher()); + var sm = new MqttClientSubscriptionsManager("", new MqttServerEventDispatcher(), new MqttServerOptions()); var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtMostOnce)); + sp.TopicFilters.Add(new TopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - sm.Subscribe(sp); + sm.SubscribeAsync(sp).GetAwaiter().GetResult(); var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce); Assert.IsTrue(result.IsSubscribed); @@ -41,13 +41,13 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeTwoTimesSuccess() { - var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServerEventDispatcher()); + var sm = new MqttClientSubscriptionsManager("", new MqttServerEventDispatcher(), new MqttServerOptions()); var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new TopicFilter("#", MqttQualityOfServiceLevel.AtMostOnce)); - sp.TopicFilters.Add(new TopicFilter("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce)); + sp.TopicFilters.Add(new TopicFilter { Topic = "#", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + sp.TopicFilters.Add(new TopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - sm.Subscribe(sp); + sm.SubscribeAsync(sp).GetAwaiter().GetResult(); var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce); Assert.IsTrue(result.IsSubscribed); @@ -57,12 +57,12 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeSingleNoSuccess() { - var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServerEventDispatcher()); + var sm = new MqttClientSubscriptionsManager("", new MqttServerEventDispatcher(), new MqttServerOptions()); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp); + sm.SubscribeAsync(sp).GetAwaiter().GetResult(); Assert.IsFalse(sm.CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); } @@ -70,18 +70,18 @@ namespace MQTTnet.Core.Tests [TestMethod] public void MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() { - var sm = new MqttClientSubscriptionsManager("", new MqttServerOptions(), new MqttServerEventDispatcher()); + var sm = new MqttClientSubscriptionsManager("", new MqttServerEventDispatcher(), new MqttServerOptions()); var sp = new MqttSubscribePacket(); sp.TopicFilters.Add(new TopicFilterBuilder().WithTopic("A/B/C").Build()); - sm.Subscribe(sp); + sm.SubscribeAsync(sp).GetAwaiter().GetResult(); Assert.IsTrue(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); var up = new MqttUnsubscribePacket(); up.TopicFilters.Add("A/B/C"); - sm.Unsubscribe(up); + sm.UnsubscribeAsync(up); Assert.IsFalse(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce).IsSubscribed); } diff --git a/Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs new file mode 100644 index 00000000..b2005ba1 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs @@ -0,0 +1,75 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Implementations; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttTcpChannel_Tests + { + [TestMethod] + public async Task Dispose_Channel_While_Used() + { + var ct = new CancellationTokenSource(); + var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001)); + serverSocket.Listen(0); + +#pragma warning disable 4014 + Task.Run(async () => +#pragma warning restore 4014 + { + while (!ct.IsCancellationRequested) + { + var client = await serverSocket.AcceptAsync(); + var data = new byte[] { 128 }; + await client.SendAsync(new ArraySegment(data), SocketFlags.None); + } + }, ct.Token); + + var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await clientSocket.ConnectAsync(IPAddress.Loopback, 50001); + + await Task.Delay(100, ct.Token); + + var tcpChannel = new MqttTcpChannel(new NetworkStream(clientSocket, true), "test"); + + var buffer = new byte[1]; + await tcpChannel.ReadAsync(buffer, 0, 1, ct.Token); + + Assert.AreEqual(128, buffer[0]); + + // This block should fail after dispose. +#pragma warning disable 4014 + Task.Run(() => +#pragma warning restore 4014 + { + Task.Delay(200, ct.Token); + tcpChannel.Dispose(); + }, ct.Token); + + try + { + await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None); + } + catch (Exception exception) + { + Assert.IsInstanceOfType(exception, typeof(SocketException)); + Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode); + } + } + finally + { + ct.Cancel(false); + serverSocket.Dispose(); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs new file mode 100644 index 00000000..f192294f --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttTopicValidator_Tests + { + [TestMethod] + public void Valid_Topic() + { + MqttTopicValidator.ThrowIfInvalid("/a/b/c"); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Plus() + { + MqttTopicValidator.ThrowIfInvalid("/a/+/c"); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Hash() + { + MqttTopicValidator.ThrowIfInvalid("/a/#/c"); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Empty() + { + MqttTopicValidator.ThrowIfInvalid(string.Empty); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/RPC_Tests.cs b/Tests/MQTTnet.Core.Tests/RPC_Tests.cs new file mode 100644 index 00000000..b0babfa0 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/RPC_Tests.cs @@ -0,0 +1,55 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Tests.Mockups; +using MQTTnet.Client; +using MQTTnet.Client.Receiving; +using MQTTnet.Exceptions; +using MQTTnet.Extensions.Rpc; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests +{ + [TestClass] + public class RPC_Tests + { + [TestMethod] + public async Task Execute_Success() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + var responseSender = await testEnvironment.ConnectClientAsync(); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + + responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => + { + await responseSender.PublishAsync(e.ApplicationMessage.Topic + "/response", "pong"); + }); + + var requestSender = await testEnvironment.ConnectClientAsync(); + + var rpcClient = new MqttRpcClient(requestSender); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationTimedOutException))] + public async Task Execute_Timeout() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var requestSender = await testEnvironment.ConnectClientAsync(); + + var rpcClient = new MqttRpcClient(requestSender); + await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs b/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs deleted file mode 100644 index c405f5fe..00000000 --- a/Tests/MQTTnet.Core.Tests/RoundtripTimeTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Server; - -namespace MQTTnet.Core.Tests -{ - [TestClass] - public class RoundtripTimeTests - { - [TestMethod] - public async Task Round_Trip_Time() - { - var factory = new MqttFactory(); - var server = factory.CreateMqttServer(); - var receiverClient = factory.CreateMqttClient(); - var senderClient = factory.CreateMqttClient(); - - await server.StartAsync(new MqttServerOptionsBuilder().Build()); - - await receiverClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - await receiverClient.SubscribeAsync("#"); - - await senderClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build()); - - TaskCompletionSource response = null; - - receiverClient.ApplicationMessageReceived += (sender, args) => - { - response?.SetResult(args.ApplicationMessage.ConvertPayloadToString()); - }; - - var times = new List(); - var stopwatch = Stopwatch.StartNew(); - - for (var i = 0; i < 100; i++) - { - response = new TaskCompletionSource(); - await senderClient.PublishAsync("test", DateTime.UtcNow.Ticks.ToString()); - response.Task.GetAwaiter().GetResult(); - - stopwatch.Stop(); - times.Add(stopwatch.Elapsed); - stopwatch.Restart(); - } - - await server.StopAsync(); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs b/Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs new file mode 100644 index 00000000..02055a84 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests +{ + [TestClass] + public class RoundtripTime_Tests + { + [TestMethod] + public async Task Round_Trip_Time() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + var receiverClient = await testEnvironment.ConnectClientAsync(); + var senderClient = await testEnvironment.ConnectClientAsync(); + + await receiverClient.SubscribeAsync("#"); + + TaskCompletionSource response = null; + + receiverClient.UseApplicationMessageReceivedHandler(e => + { + response?.SetResult(e.ApplicationMessage.ConvertPayloadToString()); + }); + + var times = new List(); + var stopwatch = Stopwatch.StartNew(); + + await Task.Delay(1000); + + for (var i = 0; i < 100; i++) + { + response = new TaskCompletionSource(); + await senderClient.PublishAsync("test", DateTime.UtcNow.Ticks.ToString()); + response.Task.GetAwaiter().GetResult(); + + stopwatch.Stop(); + times.Add(stopwatch.Elapsed); + stopwatch.Restart(); + } + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/Server_Status_Tests.cs b/Tests/MQTTnet.Core.Tests/Server_Status_Tests.cs new file mode 100644 index 00000000..8111bd00 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/Server_Status_Tests.cs @@ -0,0 +1,160 @@ +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; +using MQTTnet.Client.Options; +using MQTTnet.Tests.Mockups; +using MQTTnet.Client; +using MQTTnet.Protocol; +using MQTTnet.Server; + +namespace MQTTnet.Tests +{ + [TestClass] + public class Server_Status_Tests + { + [TestMethod] + public async Task Show_Client_And_Session_Statistics() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("client1")); + var c2 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("client2")); + + await Task.Delay(500); + + var clientStatus = await server.GetClientStatusAsync(); + var sessionStatus = await server.GetSessionStatusAsync(); + + Assert.AreEqual(2, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + + Assert.IsTrue(clientStatus.Any(s => s.ClientId == "client1")); + Assert.IsTrue(clientStatus.Any(s => s.ClientId == "client2")); + + await c1.DisconnectAsync(); + await c2.DisconnectAsync(); + + await Task.Delay(500); + + clientStatus = await server.GetClientStatusAsync(); + sessionStatus = await server.GetSessionStatusAsync(); + + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(0, sessionStatus.Count); + } + } + + [TestMethod] + public async Task Disconnect_Client() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("client1")); + + await Task.Delay(500); + + var clientStatus = await server.GetClientStatusAsync(); + + Assert.AreEqual(1, clientStatus.Count); + + Assert.IsTrue(clientStatus.Any(s => s.ClientId == "client1")); + + await clientStatus.First().DisconnectAsync(); + + await Task.Delay(500); + + Assert.IsFalse(c1.IsConnected); + + clientStatus = await server.GetClientStatusAsync(); + + Assert.AreEqual(0, clientStatus.Count); + } + } + + [TestMethod] + public async Task Keep_Persistent_Session() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("client1")); + var c2 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("client2")); + + await c1.DisconnectAsync(); + + await Task.Delay(500); + + var clientStatus = await server.GetClientStatusAsync(); + var sessionStatus = await server.GetSessionStatusAsync(); + + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + + await c2.DisconnectAsync(); + + await Task.Delay(500); + + clientStatus = await server.GetClientStatusAsync(); + sessionStatus = await server.GetSessionStatusAsync(); + + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + } + } + + [TestMethod] + public async Task Track_Sent_Application_Messages() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClientAsync(); + + for (var i = 1; i < 25; i++) + { + await c1.PublishAsync("a"); + await Task.Delay(50); + + var clientStatus = await server.GetClientStatusAsync(); + Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount); + Assert.AreEqual(0, clientStatus.First().ReceivedApplicationMessagesCount); + } + } + } + + [TestMethod] + public async Task Track_Sent_Packets() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithNoKeepAlive()); + + for (var i = 1; i < 25; i++) + { + // At most once will send one packet to the client and the server will reply + // with an additional ACK packet. + await c1.PublishAsync("a", string.Empty, MqttQualityOfServiceLevel.AtLeastOnce); + await Task.Delay(50); + + var clientStatus = await server.GetClientStatusAsync(); + + Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount, "SAMC invalid!"); + + // + 1 because CONNECT is also counted. + Assert.AreEqual(i + 1, clientStatus.First().SentPacketsCount, "SPC invalid!"); + + // +1 because ConnACK package is already counted. + Assert.AreEqual(i + 1, clientStatus.First().ReceivedPacketsCount, "RPC invalid!"); + } + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/Server_Tests.cs b/Tests/MQTTnet.Core.Tests/Server_Tests.cs new file mode 100644 index 00000000..7b78e340 --- /dev/null +++ b/Tests/MQTTnet.Core.Tests/Server_Tests.cs @@ -0,0 +1,1060 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Receiving; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests +{ + [TestClass] + public class Server_Tests + { + [TestMethod] + public async Task Publish_At_Most_Once_0x00() + { + await TestPublishAsync( + "A/B/C", + MqttQualityOfServiceLevel.AtMostOnce, + "A/B/C", + MqttQualityOfServiceLevel.AtMostOnce, + 1); + } + + [TestMethod] + public async Task Publish_At_Least_Once_0x01() + { + await TestPublishAsync( + "A/B/C", + MqttQualityOfServiceLevel.AtLeastOnce, + "A/B/C", + MqttQualityOfServiceLevel.AtLeastOnce, + 1); + } + + [TestMethod] + public async Task Publish_Exactly_Once_0x02() + { + await TestPublishAsync( + "A/B/C", + MqttQualityOfServiceLevel.ExactlyOnce, + "A/B/C", + MqttQualityOfServiceLevel.ExactlyOnce, + 1); + } + + [TestMethod] + public async Task Use_Clean_Session() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCleanSession().Build()); + + Assert.IsFalse(connectResult.IsSessionPresent); + } + } + + [TestMethod] + public async Task Will_Message_Do_Not_Send() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServerAsync(); + + var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); + + var clientOptions = new MqttClientOptionsBuilder().WithWillMessage(willMessage); + + var c1 = await testEnvironment.ConnectClientAsync(); + c1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => Interlocked.Increment(ref receivedMessagesCount)); + await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); + + var c2 = await testEnvironment.ConnectClientAsync(clientOptions); + await c2.DisconnectAsync().ConfigureAwait(false); + + await Task.Delay(1000); + + Assert.AreEqual(0, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Will_Message_Send() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServerAsync(); + + var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); + + var clientOptions = new MqttClientOptionsBuilder().WithWillMessage(willMessage); + + var c1 = await testEnvironment.ConnectClientAsync(); + c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); + + var c2 = await testEnvironment.ConnectClientAsync(clientOptions); + c2.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. + + await Task.Delay(1000); + + Assert.AreEqual(1, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Subscribe_Unsubscribe() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + var server = await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("c1")); + c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + + var c2 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("c2")); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + await c2.PublishAsync(message); + + await Task.Delay(500); + Assert.AreEqual(0, receivedMessagesCount); + + var subscribeEventCalled = false; + server.ClientSubscribedTopicHandler = new MqttServerClientSubscribedHandlerDelegate(e => + { + subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == "c1"; + }); + + await c1.SubscribeAsync(new TopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + await Task.Delay(250); + Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(250); + Assert.AreEqual(1, receivedMessagesCount); + + var unsubscribeEventCalled = false; + server.ClientUnsubscribedTopicHandler = new MqttServerClientUnsubscribedTopicHandlerDelegate(e => + { + unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == "c1"; + }); + + await c1.UnsubscribeAsync("a"); + await Task.Delay(250); + Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(500); + Assert.AreEqual(1, receivedMessagesCount); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Publish_From_Server() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + + var receivedMessagesCount = 0; + + var client = await testEnvironment.ConnectClientAsync(); + client.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + await client.SubscribeAsync(new TopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + + await server.PublishAsync(message); + + await Task.Delay(1000); + + Assert.AreEqual(1, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Publish_Multiple_Clients() + { + var receivedMessagesCount = 0; + var locked = new object(); + + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(); + var c2 = await testEnvironment.ConnectClientAsync(); + + c1.UseApplicationMessageReceivedHandler(c => + { + lock (locked) + { + receivedMessagesCount++; + } + }); + + c2.UseApplicationMessageReceivedHandler(c => + { + lock (locked) + { + receivedMessagesCount++; + } + }); + + await c1.SubscribeAsync(new TopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + await c2.SubscribeAsync(new TopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + + for (var i = 0; i < 1000; i++) + { + await c1.PublishAsync(message); + } + + await Task.Delay(500); + + Assert.AreEqual(2000, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Session_Takeover() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var options = new MqttClientOptionsBuilder() + .WithCleanSession(false) + .WithClientId("a"); + + var client1 = await testEnvironment.ConnectClientAsync(options); + await Task.Delay(500); + + var client2 = await testEnvironment.ConnectClientAsync(options); + await Task.Delay(500); + + Assert.IsFalse(client1.IsConnected); + Assert.IsTrue(client2.IsConnected); + } + } + + [TestMethod] + public async Task No_Messages_If_No_Subscription() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var client = await testEnvironment.ConnectClientAsync(); + var receivedMessages = new List(); + + client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => + { + await client.PublishAsync("Connected"); + }); + + client.UseApplicationMessageReceivedHandler(c => + { + lock (receivedMessages) + { + receivedMessages.Add(c.ApplicationMessage); + } + }); + + await Task.Delay(500); + + await client.PublishAsync("Hello"); + + await Task.Delay(500); + + Assert.AreEqual(0, receivedMessages.Count); + } + } + + [TestMethod] + public async Task Set_Subscription_At_Server() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => server.SubscribeAsync(e.ClientId, "topic1")); + + var client = await testEnvironment.ConnectClientAsync(); + var receivedMessages = new List(); + + client.UseApplicationMessageReceivedHandler(c => + { + lock (receivedMessages) + { + receivedMessages.Add(c.ApplicationMessage); + } + }); + + await Task.Delay(500); + + await client.PublishAsync("Hello"); + await Task.Delay(100); + Assert.AreEqual(0, receivedMessages.Count); + + await client.PublishAsync("topic1"); + await Task.Delay(100); + Assert.AreEqual(1, receivedMessages.Count); + } + } + + [TestMethod] + public async Task Shutdown_Disconnects_Clients_Gracefully() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + + var disconnectCalled = 0; + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder()); + c1.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(e => disconnectCalled++); + + await Task.Delay(100); + + await server.StopAsync(); + + await Task.Delay(100); + + Assert.AreEqual(1, disconnectCalled); + } + } + + [TestMethod] + public async Task Handle_Clean_Disconnect() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + + var clientConnectedCalled = 0; + var clientDisconnectedCalled = 0; + + server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(_ => Interlocked.Increment(ref clientConnectedCalled)); + server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(_ => Interlocked.Increment(ref clientDisconnectedCalled)); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder()); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(0, clientDisconnectedCalled); + + await Task.Delay(500); + + await c1.DisconnectAsync(); + + await Task.Delay(500); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(1, clientDisconnectedCalled); + } + } + + [TestMethod] + public async Task Client_Disconnect_Without_Errors() + { + using (var testEnvironment = new TestEnvironment()) + { + bool clientWasConnected; + + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + try + { + var client = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder()); + + clientWasConnected = true; + + await client.DisconnectAsync(); + + await Task.Delay(500); + } + finally + { + await server.StopAsync(); + } + + Assert.IsTrue(clientWasConnected); + + testEnvironment.ThrowIfLogErrors(); + } + } + + [TestMethod] + public async Task Handle_Lots_Of_Parallel_Retained_Messages() + { + const int ClientCount = 50; + + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + + var tasks = new List(); + for (var i = 0; i < ClientCount; i++) + { + var i2 = i; + var testEnvironment2 = testEnvironment; + + tasks.Add(Task.Run(async () => + { + try + { + using (var client = await testEnvironment2.ConnectClientAsync()) + { + // Clear retained message. + await client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload(new byte[0]).WithRetainFlag().Build()); + + // Set retained message. + await client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload("value").WithRetainFlag().Build()); + + await client.DisconnectAsync(); + } + } + catch (Exception exception) + { + testEnvironment2.TrackException(exception); + } + })); + } + + await Task.WhenAll(tasks); + + await Task.Delay(1000); + + var retainedMessages = await server.GetRetainedApplicationMessagesAsync(); + + Assert.AreEqual(ClientCount, retainedMessages.Count); + + for (var i = 0; i < ClientCount; i++) + { + Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); + } + } + } + + [TestMethod] + public async Task Retained_Messages_Flow() + { + using (var testEnvironment = new TestEnvironment()) + { + var retainedMessage = new MqttApplicationMessageBuilder().WithTopic("r").WithPayload("r").WithRetainFlag().Build(); + + await testEnvironment.StartServerAsync(); + var c1 = await testEnvironment.ConnectClientAsync(); + + var receivedMessages = 0; + + var c2 = await testEnvironment.ConnectClientAsync(); + c2.UseApplicationMessageReceivedHandler(c => + { + Interlocked.Increment(ref receivedMessages); + }); + + await c1.PublishAsync(retainedMessage); + await c1.DisconnectAsync(); + await Task.Delay(500); + + for (var i = 0; i < 5; i++) + { + await c2.UnsubscribeAsync("r"); + await Task.Delay(100); + Assert.AreEqual(i, receivedMessages); + + await c2.SubscribeAsync("r"); + await Task.Delay(100); + Assert.AreEqual(i + 1, receivedMessages); + } + + await c2.DisconnectAsync(); + } + } + + [TestMethod] + public async Task Receive_No_Retained_Message_After_Subscribe() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.DisconnectAsync(); + + var receivedMessagesCount = 0; + + var c2 = await testEnvironment.ConnectClientAsync(); + c2.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained_other").Build()); + + await Task.Delay(500); + + Assert.AreEqual(0, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Receive_Retained_Message_After_Subscribe() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.DisconnectAsync(); + + var receivedMessages = new List(); + + var c2 = await testEnvironment.ConnectClientAsync(); + c2.UseApplicationMessageReceivedHandler(c => + { + lock (receivedMessages) + { + receivedMessages.Add(c.ApplicationMessage); + } + }); + + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("retained").Build()); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.IsTrue(receivedMessages.First().Retain); + } + } + + [TestMethod] + public async Task Clear_Retained_Message_With_Empty_Payload() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag().Build()); + + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClientAsync(); + + c2.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + + await Task.Delay(200); + await c2.SubscribeAsync(new TopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await Task.Delay(500); + + Assert.AreEqual(0, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Clear_Retained_Message_With_Null_Payload() + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServerAsync(); + + var c1 = await testEnvironment.ConnectClientAsync(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload((byte[])null).WithRetainFlag().Build()); + + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClientAsync(); + + c2.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + + await Task.Delay(200); + await c2.SubscribeAsync(new TopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await Task.Delay(500); + + Assert.AreEqual(0, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Intercept_Application_Message() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync( + new MqttServerOptionsBuilder().WithApplicationMessageInterceptor( + c => { c.ApplicationMessage = new MqttApplicationMessage {Topic = "new_topic" }; })); + + string receivedTopic = null; + var c1 = await testEnvironment.ConnectClientAsync(); + await c1.SubscribeAsync("#"); + c1.UseApplicationMessageReceivedHandler(a => { receivedTopic = a.ApplicationMessage.Topic; }); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); + + await Task.Delay(500); + Assert.AreEqual("new_topic", receivedTopic); + } + } + + [TestMethod] + public async Task Persist_Retained_Message() + { + var serverStorage = new TestServerStorage(); + + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithStorage(serverStorage)); + + var c1 = await testEnvironment.ConnectClientAsync(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + + await Task.Delay(500); + + Assert.AreEqual(1, serverStorage.Messages.Count); + } + } + + [TestMethod] + public async Task Publish_After_Client_Connects() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(); + server.UseClientConnectedHandler(async e => + { + await server.PublishAsync("/test/1", "true", MqttQualityOfServiceLevel.ExactlyOnce, false); + }); + + string receivedTopic = null; + + var c1 = await testEnvironment.ConnectClientAsync(); + c1.UseApplicationMessageReceivedHandler(e => { receivedTopic = e.ApplicationMessage.Topic; }); + await c1.SubscribeAsync("#"); + + await testEnvironment.ConnectClientAsync(); + await testEnvironment.ConnectClientAsync(); + await testEnvironment.ConnectClientAsync(); + await testEnvironment.ConnectClientAsync(); + + await Task.Delay(500); + + Assert.AreEqual("/test/1", receivedTopic); + } + } + + [TestMethod] + public async Task Intercept_Message() + { + void Interceptor(MqttApplicationMessageInterceptorContext context) + { + context.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); + } + + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithApplicationMessageInterceptor(Interceptor)); + + var c1 = await testEnvironment.ConnectClientAsync(); + var c2 = await testEnvironment.ConnectClientAsync(); + await c2.SubscribeAsync(new TopicFilterBuilder().WithTopic("test").Build()); + + var isIntercepted = false; + c2.UseApplicationMessageReceivedHandler(c => + { + isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(c.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; + }); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); + await c1.DisconnectAsync(); + + await Task.Delay(500); + + Assert.IsTrue(isIntercepted); + } + } + + [TestMethod] + public async Task Send_Long_Body() + { + using (var testEnvironment = new TestEnvironment()) + { + const int PayloadSizeInMB = 30; + const int CharCount = PayloadSizeInMB * 1024 * 1024; + + var longBody = new byte[CharCount]; + byte @char = 32; + + for (long i = 0; i < PayloadSizeInMB * 1024L * 1024L; i++) + { + longBody[i] = @char; + + @char++; + + if (@char > 126) + { + @char = 32; + } + } + + byte[] receivedBody = null; + + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + var client1 = await testEnvironment.ConnectClientAsync(); + client1.UseApplicationMessageReceivedHandler(c => + { + receivedBody = c.ApplicationMessage.Payload; + }); + + await client1.SubscribeAsync("string"); + + var client2 = await testEnvironment.ConnectClientAsync(); + await client2.PublishAsync("string", longBody); + + await Task.Delay(500); + + Assert.IsTrue(longBody.SequenceEqual(receivedBody ?? new byte[0])); + } + } + + [TestMethod] + public async Task Deny_Connection() + { + var serverOptions = new MqttServerOptionsBuilder().WithConnectionValidator(context => + { + context.ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized; + }); + + using (var testEnvironment = new TestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + + await testEnvironment.StartServerAsync(serverOptions); + + try + { + await testEnvironment.ConnectClientAsync(); + Assert.Fail("An exception should be raised."); + } + catch (Exception exception) + { + if (exception is MqttConnectingFailedException connectingFailedException) + { + Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, connectingFailedException.ResultCode); + } + else + { + Assert.Fail("Wrong exception."); + } + } + } + } + + [TestMethod] + public async Task Same_Client_Id_Connect_Disconnect_Event_Order() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + + var events = new List(); + + server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(_ => + { + lock (events) + { + events.Add("c"); + } + }); + + server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(_ => + { + lock (events) + { + events.Add("d"); + } + }); + + var clientOptions = new MqttClientOptionsBuilder() + .WithClientId("same_id"); + + // c + var c1 = await testEnvironment.ConnectClientAsync(clientOptions); + + await Task.Delay(500); + + var flow = string.Join(string.Empty, events); + Assert.AreEqual("c", flow); + + // dc + var c2 = await testEnvironment.ConnectClientAsync(clientOptions); + + await Task.Delay(500); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdc", flow); + + // nothing + await c1.DisconnectAsync(); + + await Task.Delay(500); + + // d + await c2.DisconnectAsync(); + + await Task.Delay(500); + + await server.StopAsync(); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdcd", flow); + } + } + + [TestMethod] + public async Task Remove_Session() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + + var clientOptions = new MqttClientOptionsBuilder(); + var c1 = await testEnvironment.ConnectClientAsync(clientOptions); + await Task.Delay(500); + Assert.AreEqual(1, (await server.GetClientStatusAsync()).Count); + + await c1.DisconnectAsync(); + await Task.Delay(500); + + Assert.AreEqual(0, (await server.GetClientStatusAsync()).Count); + } + } + + [TestMethod] + public async Task Stop_And_Restart() + { + using (var testEnvironment = new TestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServerAsync(); + + await testEnvironment.ConnectClientAsync(); + await server.StopAsync(); + + try + { + await testEnvironment.ConnectClientAsync(); + Assert.Fail("Connecting should fail."); + } + catch (Exception) + { + } + + await server.StartAsync(new MqttServerOptionsBuilder().WithDefaultEndpointPort(testEnvironment.ServerPort).Build()); + await testEnvironment.ConnectClientAsync(); + } + } + + [TestMethod] + public async Task Close_Idle_Connection() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); + + var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await client.ConnectAsync("localhost", testEnvironment.ServerPort); + + // Don't send anything. The server should close the connection. + await Task.Delay(TimeSpan.FromSeconds(3)); + + try + { + var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); + if (receivedBytes == 0) + { + return; + } + + Assert.Fail("Receive should throw an exception."); + } + catch (SocketException) + { + } + } + } + + [TestMethod] + public async Task Send_Garbage() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); + + // Send an invalid packet and ensure that the server will close the connection and stay in a waiting state + // forever. This is security related. + var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await client.ConnectAsync("localhost", testEnvironment.ServerPort); + await client.SendAsync(Encoding.UTF8.GetBytes("Garbage"), SocketFlags.None); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + try + { + var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); + if (receivedBytes == 0) + { + return; + } + + Assert.Fail("Receive should throw an exception."); + } + catch (SocketException) + { + } + } + } + + [TestMethod] + public async Task Do_Not_Send_Retained_Messages_For_Denied_Subscription() + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithSubscriptionInterceptor(c => + { + // This should lead to no subscriptions for "n" at all. So also no sending of retained messages. + if (c.TopicFilter.Topic == "n") + { + c.AcceptSubscription = false; + } + })); + + // Prepare some retained messages. + var client1 = await testEnvironment.ConnectClientAsync(); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("y").WithPayload("x").WithRetainFlag().Build()); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("n").WithPayload("x").WithRetainFlag().Build()); + await client1.DisconnectAsync(); + + await Task.Delay(500); + + // Subscribe to all retained message types. + // It is important to do this in a range of filters to ensure that a subscription is not "hidden". + var client2 = await testEnvironment.ConnectClientAsync(); + + var buffer = new StringBuilder(); + + client2.UseApplicationMessageReceivedHandler(c => + { + lock (buffer) + { + buffer.Append(c.ApplicationMessage.Topic); + } + }); + + await client2.SubscribeAsync(new TopicFilter { Topic = "y" }, new TopicFilter { Topic = "n" }); + + await Task.Delay(500); + + Assert.AreEqual("y", buffer.ToString()); + } + } + + [TestMethod] + public async Task Collect_Messages_In_Disconnected_Session() + { + using (var testEnvironment = new TestEnvironment()) + { + var server = await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder().WithPersistentSessions()); + + // Create the session including the subscription. + var client1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("a")); + await client1.SubscribeAsync("x"); + await client1.DisconnectAsync(); + await Task.Delay(500); + + var clientStatus = await server.GetClientStatusAsync(); + Assert.AreEqual(0, clientStatus.Count); + + var client2 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("b")); + await client2.PublishAsync("x", "1"); + await client2.PublishAsync("x", "2"); + await client2.PublishAsync("x", "3"); + await client2.DisconnectAsync(); + + await Task.Delay(500); + + clientStatus = await server.GetClientStatusAsync(); + var sessionStatus = await server.GetSessionStatusAsync(); + + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + + Assert.AreEqual(3, sessionStatus.First(s => s.ClientId == "a").PendingApplicationMessagesCount); + } + } + + private static async Task TestPublishAsync( + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel, + string topicFilter, + MqttQualityOfServiceLevel filterQualityOfServiceLevel, + int expectedReceivedMessagesCount) + { + using (var testEnvironment = new TestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServerAsync(new MqttServerOptionsBuilder()); + + var c1 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("receiver")); + c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + await c1.SubscribeAsync(new TopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); + + var c2 = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithClientId("sender")); + await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + await c2.DisconnectAsync().ConfigureAwait(false); + + await Task.Delay(500); + await c1.UnsubscribeAsync(topicFilter); + await Task.Delay(500); + + Assert.AreEqual(expectedReceivedMessagesCount, receivedMessagesCount); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs b/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs deleted file mode 100644 index 2b24f42b..00000000 --- a/Tests/MQTTnet.Core.Tests/TestMqttServerAdapter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Client; -using MQTTnet.Diagnostics; -using MQTTnet.Server; - -namespace MQTTnet.Core.Tests -{ - public class TestMqttServerAdapter : IMqttServerAdapter - { - public event EventHandler ClientAccepted; - - public async Task ConnectTestClient(string clientId, MqttApplicationMessage willMessage = null) - { - var adapterA = new TestMqttCommunicationAdapter(); - var adapterB = new TestMqttCommunicationAdapter(); - adapterA.Partner = adapterB; - adapterB.Partner = adapterA; - - var client = new MqttClient( - new TestMqttCommunicationAdapterFactory(adapterA), - new MqttNetLogger()); - - FireClientAcceptedEvent(adapterB); - - var options = new MqttClientOptions - { - ClientId = clientId, - WillMessage = willMessage, - ChannelOptions = new MqttClientTcpOptions() - }; - - await client.ConnectAsync(options); - SpinWait.SpinUntil(() => client.IsConnected); - - return client; - } - - private void FireClientAcceptedEvent(IMqttChannelAdapter adapter) - { - ClientAccepted?.Invoke(this, new MqttServerAdapterClientAcceptedEventArgs(adapter)); - } - - public Task StartAsync(IMqttServerOptions options) - { - return Task.FromResult(0); - } - - public Task StopAsync() - { - return Task.FromResult(0); - } - - public void Dispose() - { - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs b/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs deleted file mode 100644 index 7636b1a2..00000000 --- a/Tests/MQTTnet.Core.Tests/TestServerExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MQTTnet.Client; -using MQTTnet.Server; -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Core.Tests -{ - public static class TestServerExtensions - { - /// - /// publishes a message with a client and waits in the server until a message with the same topic is received - /// - /// - public static async Task PublishAndWaitForAsync(this IMqttClient client, IMqttServer server, MqttApplicationMessage message) - { - var tcs = new TaskCompletionSource(); - - void Handler(object sender, MqttApplicationMessageReceivedEventArgs args) - { - if (args.ApplicationMessage.Topic == message.Topic) - { - tcs.SetResult(true); - } - } - - server.ApplicationMessageReceived += Handler; - - try - { - await client.PublishAsync(message).ConfigureAwait(false); - await tcs.Task.ConfigureAwait(false); - } - finally - { - server.ApplicationMessageReceived -= Handler; - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs b/Tests/MQTTnet.Core.Tests/TopicFilterComparer_Tests.cs similarity index 97% rename from Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs rename to Tests/MQTTnet.Core.Tests/TopicFilterComparer_Tests.cs index e91e9885..35e85047 100644 --- a/Tests/MQTTnet.Core.Tests/TopicFilterComparerTests.cs +++ b/Tests/MQTTnet.Core.Tests/TopicFilterComparer_Tests.cs @@ -1,10 +1,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Server; -namespace MQTTnet.Core.Tests +namespace MQTTnet.Tests { [TestClass] - public class TopicFilterComparerTests + public class TopicFilterComparer_Tests { [TestMethod] public void TopicFilterComparer_DirectMatch() diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj index a23f309b..b8a9307f 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj +++ b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs index db352f3c..3a58acf7 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs +++ b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using MQTTnet; using MQTTnet.AspNetCore; using MQTTnet.Server; @@ -36,7 +35,7 @@ namespace MQTTnet.TestApp.AspNetCore2 //app.UseMqttEndpoint(); app.UseMqttServer(server => { - server.Started += async (sender, args) => + server.StartedHandler = new MqttServerStartedHandlerDelegate(async args => { var msg = new MqttApplicationMessageBuilder() .WithPayload("Mqtt is awesome") @@ -58,7 +57,7 @@ namespace MQTTnet.TestApp.AspNetCore2 await Task.Delay(TimeSpan.FromSeconds(2)); } } - }; + }); }); app.Use((context, next) => diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/package.json b/Tests/MQTTnet.TestApp.AspNetCore2/package.json index 56c9018c..9363db99 100644 --- a/Tests/MQTTnet.TestApp.AspNetCore2/package.json +++ b/Tests/MQTTnet.TestApp.AspNetCore2/package.json @@ -2,13 +2,11 @@ "version": "1.0.0", "name": "mqtt.test", "private": true, - "devDependencies": { - }, - + "devDependencies": {}, "dependencies": { "mqtt": "2.15.1", "@types/node": "8.0.46", "systemjs": "0.20.19", "typescript": "2.3.4" } -} \ No newline at end of file +} diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs b/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs index 4b166236..fbeb3bfc 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Options; namespace MQTTnet.TestApp.NetCore { diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs index 98e7f0b1..09f85777 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs @@ -2,6 +2,10 @@ using System.Text; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Receiving; using MQTTnet.Protocol; namespace MQTTnet.TestApp.NetCore @@ -23,8 +27,8 @@ namespace MQTTnet.TestApp.NetCore Server = "127.0.0.1" } }; - - client.ApplicationMessageReceived += (s, e) => + + client.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => { Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}"); @@ -32,18 +36,18 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}"); Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); Console.WriteLine(); - }; + }); - client.Connected += async (s, e) => + client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => { Console.WriteLine("### CONNECTED WITH SERVER ###"); await client.SubscribeAsync(new TopicFilterBuilder().WithTopic("#").Build()); Console.WriteLine("### SUBSCRIBED ###"); - }; + }); - client.Disconnected += async (s, e) => + client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(async e => { Console.WriteLine("### DISCONNECTED FROM SERVER ###"); await Task.Delay(TimeSpan.FromSeconds(5)); @@ -56,7 +60,7 @@ namespace MQTTnet.TestApp.NetCore { Console.WriteLine("### RECONNECTING FAILED ###"); } - }; + }); try { @@ -73,14 +77,14 @@ namespace MQTTnet.TestApp.NetCore { Console.ReadLine(); - await client.SubscribeAsync(new TopicFilter("test", MqttQualityOfServiceLevel.AtMostOnce)); + await client.SubscribeAsync(new TopicFilter { Topic = "test", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); var applicationMessage = new MqttApplicationMessageBuilder() .WithTopic("A/B/C") .WithPayload("Hello World") .WithAtLeastOnceQoS() .Build(); - + await client.PublishAsync(applicationMessage); } } diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj index 0dd56d73..1900203e 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj @@ -12,7 +12,7 @@ - + diff --git a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs index 563f3430..0d4f8f9d 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs @@ -3,7 +3,8 @@ using System.Threading.Tasks; using System.IO; using Newtonsoft.Json; using System.Collections.Generic; -using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet.Client.Receiving; using MQTTnet.Extensions.ManagedClient; using MQTTnet.Protocol; @@ -14,7 +15,7 @@ namespace MQTTnet.TestApp.NetCore public static async Task RunAsync() { var ms = new ClientRetainedMessageHandler(); - + var options = new ManagedMqttClientOptions { ClientOptions = new MqttClientOptions @@ -34,19 +35,19 @@ namespace MQTTnet.TestApp.NetCore try { var managedClient = new MqttFactory().CreateManagedMqttClient(); - managedClient.ApplicationMessageReceived += (s, e) => + managedClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => { Console.WriteLine(">> RECEIVED: " + e.ApplicationMessage.Topic); - }; + }); await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("1")); await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("2").WithAtLeastOnceQoS()); await managedClient.StartAsync(options); - await managedClient.SubscribeAsync(new TopicFilter("xyz", MqttQualityOfServiceLevel.AtMostOnce)); - await managedClient.SubscribeAsync(new TopicFilter("abc", MqttQualityOfServiceLevel.AtMostOnce)); - + await managedClient.SubscribeAsync(new TopicFilter { Topic = "xyz", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await managedClient.SubscribeAsync(new TopicFilter { Topic = "abc", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("3")); Console.WriteLine("Managed client started."); diff --git a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs index 3f4a1d54..466d2066 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Protocol; using MQTTnet.Server; @@ -159,7 +160,7 @@ namespace MQTTnet.TestApp.NetCore msgCount += msgs.Count; //send multiple } - + var now = DateTime.Now; if (last < now - TimeSpan.FromSeconds(1)) { @@ -192,5 +193,158 @@ namespace MQTTnet.TestApp.NetCore Interlocked.Increment(ref count); return Task.Run(() => client.PublishAsync(applicationMessage)); } + + public static async Task RunQoS2Test() + { + try + { + var mqttServer = new MqttFactory().CreateMqttServer(); + await mqttServer.StartAsync(new MqttServerOptions()); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + }, + CleanSession = true + }; + + var client = new MqttFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + Payload = Encoding.UTF8.GetBytes("Hello World"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + stopwatch.Restart(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + public static async Task RunQoS1Test() + { + try + { + var mqttServer = new MqttFactory().CreateMqttServer(); + await mqttServer.StartAsync(new MqttServerOptions()); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + }, + CleanSession = true + }; + + var client = new MqttFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + Payload = Encoding.UTF8.GetBytes("Hello World"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + stopwatch.Restart(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + public static async Task RunQoS0Test() + { + try + { + var mqttServer = new MqttFactory().CreateMqttServer(); + await mqttServer.StartAsync(new MqttServerOptions()); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + Server = "127.0.0.1" + }, + CleanSession = true + }; + + var client = new MqttFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + Payload = Encoding.UTF8.GetBytes("Hello World"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + stopwatch.Restart(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } } } diff --git a/Tests/MQTTnet.TestApp.NetCore/Program.cs b/Tests/MQTTnet.TestApp.NetCore/Program.cs index 908844fa..777c6c07 100644 --- a/Tests/MQTTnet.TestApp.NetCore/Program.cs +++ b/Tests/MQTTnet.TestApp.NetCore/Program.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Diagnostics; using MQTTnet.Server; using Newtonsoft.Json; @@ -24,6 +27,9 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine("7 = Client flow test"); Console.WriteLine("8 = Start performance test (client only)"); Console.WriteLine("9 = Start server (no trace)"); + Console.WriteLine("a = Start QoS 2 benchmark"); + Console.WriteLine("b = Start QoS 1 benchmark"); + Console.WriteLine("c = Start QoS 0 benchmark"); var pressedKey = Console.ReadKey(true); if (pressedKey.KeyChar == '1') @@ -65,61 +71,21 @@ namespace MQTTnet.TestApp.NetCore ServerTest.RunEmptyServer(); return; } + else if (pressedKey.KeyChar == 'a') + { + Task.Run(PerformanceTest.RunQoS2Test); + } + else if (pressedKey.KeyChar == 'b') + { + Task.Run(PerformanceTest.RunQoS1Test); + } + else if (pressedKey.KeyChar == 'c') + { + Task.Run(PerformanceTest.RunQoS0Test); + } Thread.Sleep(Timeout.Infinite); } - - // This code is used at the Wiki on GitHub! - // ReSharper disable once UnusedMember.Local - private static async void WikiCode() - { - { - var client = new MqttFactory().CreateMqttClient(); - - var options = new MqttClientOptionsBuilder() - .WithClientId("Client1") - .WithTcpServer("broker.hivemq.com") - .WithCredentials("bud", "%spencer%") - .WithTls() - .Build(); - - await client.ConnectAsync(options); - - var message = new MqttApplicationMessageBuilder() - .WithTopic("MyTopic") - .WithPayload("Hello World") - .WithExactlyOnceQoS() - .WithRetainFlag() - .Build(); - - await client.PublishAsync(message); - } - - { - var factory = new MqttFactory(); - var client = factory.CreateMqttClient(); - } - - { - // Write all trace messages to the console window. - MqttNetGlobalLogger.LogMessagePublished += (s, e) => - { - var trace = $">> [{e.TraceMessage.Timestamp:O}] [{e.TraceMessage.ThreadId}] [{e.TraceMessage.Source}] [{e.TraceMessage.Level}]: {e.TraceMessage.Message}"; - if (e.TraceMessage.Exception != null) - { - trace += Environment.NewLine + e.TraceMessage.Exception.ToString(); - } - - Console.WriteLine(trace); - }; - } - - { - // Use a custom log ID for the logger. - var factory = new MqttFactory(); - var mqttClient = factory.CreateMqttClient(new MqttNetLogger("MyCustomId")); - } - } } public class RetainedMessageHandler : IMqttServerStorage @@ -154,4 +120,22 @@ namespace MQTTnet.TestApp.NetCore return Task.FromResult(retainedMessages); } } + + public class WikiCode + { + public void Code() + { + //Validate certificate. + var options = new MqttClientOptionsBuilder() + .WithTls(new MqttClientOptionsBuilderTlsParameters + { + CertificateValidationCallback = (X509Certificate x, X509Chain y, SslPolicyErrors z, IMqttClientOptions o) => + { + // TODO: Check conditions of certificate by using above parameters. + return true; + } + }) + .Build(); + } + } } \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs b/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs index e46ed168..ff4e3374 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs @@ -3,7 +3,9 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics; +using MQTTnet.Client.Options; +using MQTTnet.Client.Receiving; +using MQTTnet.Formatter; using MQTTnet.Protocol; using Newtonsoft.Json; @@ -17,37 +19,37 @@ namespace MQTTnet.TestApp.NetCore // iot.eclipse.org await ExecuteTestAsync("iot.eclipse.org TCP", - new MqttClientOptionsBuilder().WithTcpServer("iot.eclipse.org", 1883).Build()); + new MqttClientOptionsBuilder().WithTcpServer("iot.eclipse.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("iot.eclipse.org WS", - new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:80/mqtt").Build()); + new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:80/mqtt").WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("iot.eclipse.org WS TLS", - new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:443/mqtt").WithTls().Build()); + new MqttClientOptionsBuilder().WithWebSocketServer("iot.eclipse.org:443/mqtt").WithProtocolVersion(MqttProtocolVersion.V311).WithTls().Build()); // test.mosquitto.org await ExecuteTestAsync("test.mosquitto.org TCP", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).Build()); + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("test.mosquitto.org TCP TLS", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithTls().Build()); + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTls().Build()); await ExecuteTestAsync("test.mosquitto.org WS", - new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8080/mqtt").Build()); + new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8080/mqtt").WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("test.mosquitto.org WS TLS", - new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8081/mqtt").WithTls().Build()); + new MqttClientOptionsBuilder().WithWebSocketServer("test.mosquitto.org:8081/mqtt").WithProtocolVersion(MqttProtocolVersion.V311).WithTls().Build()); // broker.hivemq.com await ExecuteTestAsync("broker.hivemq.com TCP", - new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).Build()); + new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("broker.hivemq.com WS", - new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").Build()); + new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").WithProtocolVersion(MqttProtocolVersion.V311).Build()); // mqtt.swifitch.cz await ExecuteTestAsync("mqtt.swifitch.cz", - new MqttClientOptionsBuilder().WithTcpServer("mqtt.swifitch.cz", 1883).Build()); + new MqttClientOptionsBuilder().WithTcpServer("mqtt.swifitch.cz", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); // CloudMQTT var configFile = Path.Combine("E:\\CloudMqttTestConfig.json"); @@ -56,13 +58,13 @@ namespace MQTTnet.TestApp.NetCore var config = JsonConvert.DeserializeObject(File.ReadAllText(configFile)); await ExecuteTestAsync("CloudMQTT TCP", - new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.Port).WithCredentials(config.Username, config.Password).Build()); + new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.Port).WithCredentials(config.Username, config.Password).WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("CloudMQTT TCP TLS", - new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.SslPort).WithCredentials(config.Username, config.Password).WithTls().Build()); + new MqttClientOptionsBuilder().WithTcpServer(config.Server, config.SslPort).WithCredentials(config.Username, config.Password).WithTls().WithProtocolVersion(MqttProtocolVersion.V311).Build()); await ExecuteTestAsync("CloudMQTT WS TLS", - new MqttClientOptionsBuilder().WithWebSocketServer(config.Server + ":" + config.SslWebSocketPort + "/mqtt").WithCredentials(config.Username, config.Password).WithTls().Build()); + new MqttClientOptionsBuilder().WithWebSocketServer(config.Server + ":" + config.SslWebSocketPort + "/mqtt").WithCredentials(config.Username, config.Password).WithTls().WithProtocolVersion(MqttProtocolVersion.V311).Build()); } Write("Finished.", ConsoleColor.White); @@ -79,7 +81,7 @@ namespace MQTTnet.TestApp.NetCore var topic = Guid.NewGuid().ToString(); MqttApplicationMessage receivedMessage = null; - client.ApplicationMessageReceived += (s, e) => receivedMessage = e.ApplicationMessage; + client.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => receivedMessage = e.ApplicationMessage); await client.ConnectAsync(options); await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce); @@ -92,7 +94,7 @@ namespace MQTTnet.TestApp.NetCore throw new Exception("Message invalid."); } - await client.UnsubscribeAsync("test"); + await client.UnsubscribeAsync(topic); await client.DisconnectAsync(); Write("[OK]\n", ConsoleColor.Green); diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs index 30ba9140..acd01b16 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; +using MQTTnet.Client.Options; using MQTTnet.Server; namespace MQTTnet.TestApp.NetCore diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs index 12fa1aa7..bd626719 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs +++ b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs @@ -1,6 +1,7 @@ using System; using System.Text; using System.Threading.Tasks; +using MQTTnet.Client.Receiving; using MQTTnet.Protocol; using MQTTnet.Server; @@ -23,7 +24,7 @@ namespace MQTTnet.TestApp.NetCore { var options = new MqttServerOptions { - ConnectionValidator = p => + ConnectionValidator = new MqttServerConnectionValidatorDelegate(p => { if (p.ClientId == "SpecialClient") { @@ -32,11 +33,11 @@ namespace MQTTnet.TestApp.NetCore p.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; } } - }, + }), Storage = new RetainedMessageHandler(), - ApplicationMessageInterceptor = context => + ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(context => { if (MqttTopicFilterComparer.IsMatch(context.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#")) { @@ -50,8 +51,9 @@ namespace MQTTnet.TestApp.NetCore context.AcceptPublish = false; context.CloseConnection = true; } - }, - SubscriptionInterceptor = context => + }), + + SubscriptionInterceptor = new MqttServerSubscriptionInterceptorDelegate(context => { if (context.TopicFilter.Topic.StartsWith("admin/foo/bar") && context.ClientId != "theAdmin") { @@ -63,7 +65,7 @@ namespace MQTTnet.TestApp.NetCore context.AcceptSubscription = false; context.CloseConnection = true; } - } + }) }; // Extend the timestamp for all messages from clients. @@ -77,12 +79,12 @@ namespace MQTTnet.TestApp.NetCore var mqttServer = new MqttFactory().CreateMqttServer(); - mqttServer.ApplicationMessageReceived += (s, e) => + mqttServer.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => { MqttNetConsoleLogger.PrintToConsole( $"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'", ConsoleColor.Magenta); - }; + }); //options.ApplicationMessageInterceptor = c => //{ @@ -106,10 +108,10 @@ namespace MQTTnet.TestApp.NetCore // } //}; - mqttServer.ClientDisconnected += (s, e) => + mqttServer.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => { Console.Write("Client disconnected event fired."); - }; + }); await mqttServer.StartAsync(options); diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/App.xaml.cs b/Tests/MQTTnet.TestApp.UniversalWindows/App.xaml.cs index 3ed0757a..13b5d0b4 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/App.xaml.cs +++ b/Tests/MQTTnet.TestApp.UniversalWindows/App.xaml.cs @@ -15,28 +15,14 @@ namespace MQTTnet.TestApp.UniversalWindows Suspending += OnSuspending; } - /// - /// Invoked when the application is launched normally by the end user. Other entry points - /// will be used such as when the application is launched to open a specific file. - /// - /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs e) { - // Do not repeat app initialization when the Window already has content, - // just ensure that the window is active if (!(Window.Current.Content is Frame rootFrame)) { - // Create a Frame to act as the navigation context and navigate to the first page rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; - if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) - { - //TODO: Load state from previously suspended application - } - - // Place the frame in the current Window Window.Current.Content = rootFrame; } @@ -44,37 +30,21 @@ namespace MQTTnet.TestApp.UniversalWindows { if (rootFrame.Content == null) { - // When the navigation stack isn't restored navigate to the first page, - // configuring the new page by passing required information as a navigation - // parameter rootFrame.Navigate(typeof(MainPage), e.Arguments); } - // Ensure the current window is active + Window.Current.Activate(); } } - - /// - /// Invoked when Navigation to a certain page fails - /// - /// The Frame which failed navigation - /// Details about the navigation failure + void OnNavigationFailed(object sender, NavigationFailedEventArgs e) { throw new Exception("Failed to load Page " + e.SourcePageType.FullName); } - /// - /// Invoked when application execution is being suspended. Application state is saved - /// without knowing whether the application will be terminated or resumed with the contents - /// of memory still intact. - /// - /// The source of the suspend request. - /// Details about the suspend request. private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); - //TODO: Save application state and stop any background activity deferral.Complete(); } } diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj index 82e54c44..c13db930 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MQTTnet.TestApp.UniversalWindows.csproj @@ -12,7 +12,7 @@ en-US UAP 10.0.17134.0 - 10.0.16299.0 + 10.0.10240.0 14 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -142,7 +142,7 @@ - 6.1.9 + 6.2.8 4.0.0 diff --git a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml index c99ebd84..210f0595 100644 --- a/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml +++ b/Tests/MQTTnet.TestApp.UniversalWindows/MainPage.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:server="using:MQTTnet.Server" + xmlns:status="using:MQTTnet.Server.Status" d:DesignHeight="800" d:DesignWidth="800" mc:Ignorable="d"> @@ -37,6 +38,7 @@ + Server: @@ -57,12 +59,19 @@ Keep Alive interval: - Protocol: + Transport: - TCP - WS + TCP + WS Use TLS + + Protocol: + + 5.0.0 + 3.1.1 + 3.1.0 + Clean session @@ -93,26 +102,35 @@ + + + Topic: - Payload: - + Content type: + - Payload format: - + Payload: + + + Payload format: + Plain text Base64 string - QoS level: - + QoS level: + 0 (At most once) 1 (At least once) 2 (Exactly once) + + Response topic: + Retain @@ -211,7 +229,7 @@ - +