Compare commits

...
60 Commits
Author SHA1 Message Date
christian 740d605fd2 Fix Unit Tests 2024-09-07 11:32:14 +02:00
christian 9177b6ae68 Fix Unit Tests 2024-09-07 11:22:43 +02:00
Christian f0ea5c9ecd Fix encoding and decoding of AUTH paket according to RFC. (#2076) 2024-09-07 10:52:53 +02:00
Christian 76479ebabe Setup nuget and code signing for version 4.x.x (#2069)
* Update CI

* Update CI

* Update release notes
2024-08-24 14:58:13 +02:00
pigwing e18a91a4b5 fix LoadingRetainedMessageAsync not exec (#2025)
because IHostedService StartAsync before startup Configure
see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-8.0#ihostapplicationlifetime
2024-06-22 15:24:25 +02:00
drelidan7 b95d352236 Switch SubscribeAsync/UnsubscribeAsync to IEnumerable<string> (#2026)
The function currently requires an ICollection<string>, but doesn't actually depend on the ICollection<string> interface.  Relax the requirement to be IEnumerable<string> for better flexibility.
2024-06-22 14:55:20 +02:00
simonthum c5f4cf1f9b Topic template extension maintenance (#2022)
* Topic templates: Ensure parameter values are checked for wildcard

* Topic templates: Complement extension methods

- add template.BuildMessage()
- add subscribeOptionsBuilder.WithTopicTemplate()

* Topic templates: switch some samples to use a topic template

The changes should be self-explanatory and are
intended to provide more examples.

* Topic templates: add README
2024-06-22 14:54:11 +02:00
Josua Jäger 8650002523 fix Managed_Client_Subscribe_Samples.Connect_Client sample: (#2023)
- Result was never logged (`bool subscribed`)
- `subscribed` was never set, as it was not passed as a reference but as a value.
2024-06-21 19:05:12 +02:00
Christian 25c1da6a21 Restore behavior of _Server_ and _Port_ in client options (#2009)
* Restore old server and port behavior

* Fix unit test
2024-05-23 19:32:22 +02:00
christian 4e3ede42c6 Increase version 2024-05-21 22:36:34 +02:00
Christian 918a3c8d8b Restore old signing to get out hotfix release (#2004) 2024-05-19 16:39:49 +02:00
Christian 290419b3b6 [Client] Fix endpoint handling (#1999)
* Fix endpoint handling

* Fix different behavior for Mono framework.

* Restore old properties to avoid breaking changes
2024-05-19 12:18:12 +02:00
Christian 32b6146532 Setup nuget signing (#2001)
* Setup signing

* Update release notes
2024-05-18 20:21:09 +02:00
Christian 3a73ec21f7 [Client] Restore _WithConnectionUri_ methods (#1998)
Remove obsolete attributes
2024-05-17 17:49:33 +02:00
Christian 41d5b70344 Fix support for unix sockets (#1995)
* Add support for changing the protocol type on socket level

* Do not set NoDelay when not supported by protocol

* Update release notes

* Fix release notes
2024-05-14 18:09:33 +02:00
yztandChristian b4769778af DISCONNECT packet should allow zero-length body (#1994)
* DISCONNECT packet should allow zero-length body

> [If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used.](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901208)

* Update release notes and add comment

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-05-14 18:08:54 +02:00
christian 473f9c31b1 Fix unit tests 2024-05-12 11:37:33 +02:00
christian ae4f168066 Fix build 2024-05-12 11:23:03 +02:00
christian c5578a93e8 Fix Unit Tests 2024-05-11 11:21:11 +02:00
christian f4b2a10616 Fix build errors and several warnings 2024-05-09 18:39:08 +02:00
ed63aa4910 Fix comment typos/text, refactor if-else chains to switch statements, optimize return statements, and invert conditional logic (#1961)
* Fix comment typos and refactor if-else chains to switch statements.

* Refactor code

---------

Co-authored-by: Wilmar Arias <wilmar.arias@keylinx.se>
Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-09 18:21:00 +02:00
dcharlesandchristian 4481869d42 fix: time out and cancel propagation in managed client (#1987)
* fix: time out and cancel propagation in managed client

* Update release notes

* Refactor code

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-07 19:16:32 +02:00
fazhoandchristian abbf14b1ec rethrow MqttClientUnexpectedDisconnectReceivedException when Publish … (#1974)
* rethrow MqttClientUnexpectedDisconnectReceivedException when Publish errors out because server disconnects the client and QoS is 0

* use a local var to cache _unexpectedDisconnectPacket

* Add inner exception to new exception

* Update release notes

* set the default inner exception as null

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-05-07 18:31:16 +02:00
Christian 4f9e54ba1d Expose certificate selection in client options (#1984)
* Expose certificate selection in MQTTnet options

* Fix .NET Framework builds

* Update release notes
2024-05-02 22:05:33 +02:00
Christian d80188167a Restore support for null in _AddServer_ method in ASP.NET integration (#1981)
* Upgrade ASP test project to dotnet8.

* Fix broken null support for _AddMqttServer_ method.

* Fix wrong code style

* Downgrade to dotnet7.0

* Expose certificate selection in MQTTnet options

* Revert "Expose certificate selection in MQTTnet options"

This reverts commit f7d098a119.

* Update release notes
2024-05-01 20:08:25 +02:00
Christian 4ab956abd1 Change behavior of NoMatchingSubscribers (#1975)
Change behavior of NoMatchingSubscribers.
2024-04-26 18:35:39 +02:00
simonthum 1aa91e3828 Fix unit tests for topic templates (#1976) 2024-04-26 18:35:27 +02:00
simonthumandchristian 2410d7b4c1 Utility class supporting dynamic topics using topic templates (#1932)
* Add MqttTopicTemplate

The topic template class is a way to deal with dynamic topics that
is less cumbersome and error-prone than String.Format or
other string fu one might use to handle dynamic topics.

Example:

  MyCity/{street}/{crossing}/lights

'street' and 'crossing' would be parameters.

The class and its extension methods are intended to support
dynamic subscription/publication, message-topic matching and
routing. The template syntax is modelled after the well-known
moustache syntax, which conincidentally is also published
as the 'AsyncAPI channel address expressions'.

* Add MQTTnet.Extensions.TopicTemplate to solution

* Apply code style from team shared resharper settings.
Add missing file headers.

* Update release notes

---------

Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2024-04-23 20:09:56 +02:00
Guiorgy b7328e5fd2 fixed a comment typo (#1973) 2024-04-23 20:09:39 +02:00
christian 7f5c437c46 Fix unit tests 2024-04-11 22:26:39 +02:00
christian ae5e038548 Merge remote-tracking branch 'origin/master' 2024-04-09 21:34:31 +02:00
christian 67e0fb8b0b Fix build errors by no longer using not supported language features for certain TFMs. 2024-04-09 21:34:15 +02:00
Marc PiulachsandChristian b763542329 Move "Client is not connected" error to it's own exception (#1954)
* Move "Client is not connected" error to it's own exception

* Update MqttClientNotConnectedException.cs

* Update release notes

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-04-09 21:26:59 +02:00
christian d061da6077 Update release notes 2024-04-09 21:24:39 +02:00
Andriy Tolstoy 82a235aef4 remove select method (#1959) 2024-04-09 21:18:09 +02:00
James Thompson fd55982d98 #1957 Invert the logic to improve maintainance (#1958)
#1957 Invert the logic to improve maintaince
2024-04-01 22:13:48 +02:00
aabf180b23 1862 Add subscribe and unsubscribe results event for Managed Mqtt Client. (#1894)
* See the issue #1862

Adding an event when ManagedMqttClient actually subscribe/unsubscribe to topics.
This event has to be used in parallel with connected event to know the exact state.

* Rename SubscriptionsResultAsync to SubscriptionsChangedAsync.

* Rename internal field for event

* Update ReleaseNotes.md

---------

Co-authored-by: Pierre Dufrene <pierre.dufrene@se.com>
Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-03-02 09:58:42 +01:00
Christian 1245d8299a Merge branch 'master' of https://github.com/dotnet/MQTTnet 2024-03-02 08:45:03 +01:00
yztandChristian 1ffffaff47 Fix minor bug in MqttV5PacketEncoder.cs (#1939)
* Fix minor bug in MqttV5PacketEncoder.cs

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2024-03-02 09:44:14 +01:00
Christian c204f6d8b2 Make packet inspector fully async (#1941)
* Make packet inspector methods async.

* Update release notes
2024-03-02 09:28:56 +01:00
Christian b775fb256b Update version 2024-03-02 08:44:57 +01:00
Christian f82d382f23 Merge remote-tracking branch 'origin/master' 2024-02-14 17:54:04 +01:00
Christian 4214af283a Update ReleaseNotes.md 2024-02-14 17:47:29 +01:00
Christian b32a5c9fa0 Update ReleaseNotes.md 2024-02-14 17:44:16 +01:00
Sean Hanna e7e13a631f implement intercept publish message event on ManagedMqttClient (#1915)
* implement intercept publish message event on ManagedMqttClient

* rename ShouldPublish to ACceptPublish per PR feedback
2024-02-14 17:43:45 +01:00
Christian c12096705f 1908 support unix domain sockets (#1919)
* Add support for custom endpoints

Add overlaods etc. to allow for passing other endpoint implementations

* Extend publich broker test to also probe with different protocol versions

* Fix UWP build

* Update ReleaseNotes.md
2024-02-06 17:37:07 +01:00
Christian eb25500f50 1889 no service for type mqttnetservermqttserver has been registered (#1892)
* Add missing service to service collection

* Update ReleaseNotes.md

* Restore service registrations
2023-12-08 18:01:19 +01:00
Christian 4df06980a1 1883 in a weak network environment the mqtt broker is unable to detect disconnections (#1891)
* Add new setting

* Align keep alive handling with RFC

Only completely sent packets are treated as keep alive

* Update ReleaseNotes.md
2023-12-07 17:49:03 +01:00
Christian f3499013f4 Increase version 2023-12-07 17:48:14 +01:00
Christian 158a7fca46 Reset release notes 2023-12-07 17:47:57 +01:00
kallayjandChristian 09ab30fb0f Adds events for delivered and dropped messages. (#1866)
* Adds events for delivered and dropped messages.

* Rename new events to match other namings

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-12-06 18:02:48 +01:00
Christian ae84aa43d2 1846 server mqttserverstopasync doesnt indicate correct reason (#1872) 2023-11-25 16:17:16 +01:00
RidoandChristian b431a5d3fe Add ReasonCode to SendExtendedAuthenticationExchangeDataAsync (#1882)
* add reason code to ext auth

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-11-25 15:45:10 +01:00
Christian b9683c2943 Improve client disconnect (#1873)
Imrpove client disconnect
2023-11-05 15:15:36 +01:00
Christian 1ba8f74555 Update version 2023-11-04 13:40:28 +01:00
Christian ec637f7946 Handle unobserved tasks exceptions (#1871)
* Access exception so that it will not treated as unhandled

* Refactor code

* Improve unit tests

* Fix null reference exception

* Update ReleaseNotes.md
2023-11-04 13:39:16 +01:00
Christian 2599b3dfdc Update settings for tests project so that it will build on linux (#1860)
* Update settings for tests project so that it will build on linux

* Improve unit tests and samples

* Remove no longer required target frameworks for unit tests

* Update ci.yml
2023-11-01 09:34:18 +01:00
Christian d22caa7b70 Update readme (#1870)
* Update ReleaseNotes.md

* Apply project code style
2023-11-01 09:26:18 +01:00
Rido 01c90badf9 Support custom CA chain validation (#1851)
* Add CaFile based on SslStream CustomTrust

* change to cert collection

* do not require to disable revocation checks
2023-11-01 09:22:13 +01:00
Diego2098andChristian e4f605fbd5 Update MqttServer.cs (#1858)
* Update MqttServer.cs

clientID is null => RetainedMessageChangedEventArgs ctor throw new ArgumentNullException(nameof(clientId)

* Update ReleaseNotes.md

---------

Co-authored-by: Christian <6939810+chkr1011@users.noreply.github.com>
2023-11-01 08:29:19 +01:00
101 changed files with 3508 additions and 949 deletions
+1 -2
View File
@@ -40,7 +40,6 @@ dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_property = false:suggestion
# ReSharper properties # ReSharper properties
resharper_autodetect_indent_settings = true
resharper_default_private_modifier = implicit resharper_default_private_modifier = implicit
resharper_place_accessorholder_attribute_on_same_line = false resharper_place_accessorholder_attribute_on_same_line = false
resharper_use_indent_from_vs = false resharper_use_indent_from_vs = false
@@ -61,7 +60,7 @@ resharper_web_config_wrong_module_highlighting = warning
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = crlf end_of_line = crlf
trim_trailing_whitespace = false trim_trailing_whitespace = true
insert_final_newline = false insert_final_newline = false
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
+5 -2
View File
@@ -1,2 +1,5 @@
* [Client] Fixed wrong TLS options handling (#1830). * Core: Fixed issue when parsing AUTH packet with 0 length body (#2039).
* [Client] Fixed NullReferenceExeption when performing a Ping when the client is not connected (#1831). * nuget: Changed code signing and nuget certificate (**BREAKING CHANGE**).
* TopicTemplates: Updated samples, parameter validation (#2022).
* ManagedClient: Switch SubscribeAsync/UnsubscribeAsync to IEnumerable<string> (#2026).
* Server: Fix _LoadingRetainedMessageAsync_ not executed (#2025).
+31 -13
View File
@@ -3,7 +3,7 @@ name: CI
on: [push, pull_request] on: [push, pull_request]
env: env:
VERSION: "4.3.1.${{github.run_number}}" VERSION: "4.3.7.${{github.run_number}}"
jobs: jobs:
build: build:
@@ -17,7 +17,7 @@ jobs:
sdk-version: 18362 sdk-version: 18362
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@v1.9.0 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: | dotnet-version: |
3.1.x 3.1.x
@@ -28,7 +28,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.1 uses: microsoft/setup-msbuild@v1.1
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Signing Certificate - name: Setup Signing Certificate
run: | run: |
@@ -42,23 +42,41 @@ jobs:
- name: Build solution - name: Build solution
run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk
- name: Collect nuget Packages
uses: actions/upload-artifact@v2
with:
name: nuget Packages
path: |
**\*.nupkg
**\*.snupkg
- name: Setup VSTest - name: Setup VSTest
uses: darenm/Setup-VSTest@v1 uses: darenm/Setup-VSTest@v1
- name: Core Tests - name: Core Tests
run: vstest.console.exe Source\MQTTnet.Tests\bin\Release\net6.0\MQTTnet.Tests.dll run: vstest.console.exe Source\MQTTnet.Tests\bin\Release\net7.0\MQTTnet.Tests.dll
- name: ASP.NET Tests - name: ASP.NET Tests
run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll
- name: Install sign CLI tool
run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3
- name: Sign nugets
shell: pwsh
run: >
./sign code azure-key-vault `
"**/*.nupkg" `
--base-directory "${{ github.workspace }}/Source" `
--publisher-name "MQTTnet" `
--description "MQTTnet" `
--description-url "https://github.com/dotnet/MQTTnet" `
--azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" `
--azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" `
--azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" `
--azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" `
--azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}"
- name: Upload signed nuget packages
uses: actions/upload-artifact@v4
with:
name: signed-nugets
path: |
${{ github.workspace }}/Source/**/*.nupkg
${{ github.workspace }}/Source/**/*.snupkg
- name: Publish MyGet nugets - name: Publish MyGet nugets
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
run: dotnet nuget push **/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate run: dotnet nuget push ${{ github.workspace }}/Source/**/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
+15 -9
View File
@@ -7,9 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\M
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
LICENSE = LICENSE LICENSE = LICENSE
README.md = README.md README.md = README.md
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md .github\workflows\ReleaseNotes.md = .github\workflows\ReleaseNotes.md
.github\workflows\ci.yml = .github\workflows\ci.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
@@ -20,17 +22,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.TopicTemplate", "Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj", "{374D5861-F7A6-42A2-8CBE-040846000EF9}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -82,12 +86,14 @@ Global
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
EndGlobalSection EndGlobalSection
+2
View File
@@ -230,6 +230,7 @@
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">Licensed to the .NET Foundation under one or more agreements. <s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">Licensed to the .NET Foundation under one or more agreements.
The .NET Foundation licenses this file to you under the MIT license. The .NET Foundation licenses this file to you under the MIT license.
See the LICENSE file in the project root for more information.</s:String> See the LICENSE file in the project root for more information.</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
@@ -240,6 +241,7 @@ See the LICENSE file in the project root for more information.</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=catched/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CONNACK/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=CONNACK/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PINGREQ/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=PINGREQ/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PINGRESP/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=PINGRESP/@EntryIndexedValue">True</s:Boolean>
@@ -7,6 +7,7 @@
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using System.Security.Authentication; using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Extensions.WebSocket4Net; using MQTTnet.Extensions.WebSocket4Net;
using MQTTnet.Formatter; using MQTTnet.Formatter;
@@ -435,4 +436,54 @@ public static class Client_Connection_Samples
Console.ReadLine(); Console.ReadLine();
} }
} }
public static async Task ConnectTls_WithCaFile()
{
var mqttFactory = new MqttFactory();
X509Certificate2Collection caChain = new X509Certificate2Collection();
caChain.ImportFromPem(mosquitto_org); // from https://test.mosquitto.org/ssl/mosquitto.org.crt
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("test.mosquitto.org", 8883)
.WithTlsOptions(new MqttClientTlsOptionsBuilder()
.WithTrustChain(caChain)
.Build())
.Build();
var connAck = await mqttClient.ConnectAsync(mqttClientOptions);
Console.WriteLine("Connected to test.moquitto.org:8883 with CaFile mosquitto.org.crt: " + connAck.ResultCode);
}
}
const string mosquitto_org = @"
-----BEGIN CERTIFICATE-----
MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL
BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG
A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU
BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv
by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE
BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES
MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp
dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg
UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW
Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA
s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH
3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo
E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT
MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV
6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC
6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf
+pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK
sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839
LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE
m/XriWr/Cq4h/JfB7NTsezVslgkBaoU=
-----END CERTIFICATE-----
";
} }
+16 -31
View File
@@ -8,6 +8,7 @@
// ReSharper disable UnusedMember.Local // ReSharper disable UnusedMember.Local
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Extensions.TopicTemplate;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Samples.Helpers; using MQTTnet.Samples.Helpers;
@@ -16,6 +17,8 @@ namespace MQTTnet.Samples.Client;
public static class Client_Subscribe_Samples public static class Client_Subscribe_Samples
{ {
static MqttTopicTemplate sampleTemplate = new MqttTopicTemplate("mqttnet/samples/topic/{id}");
public static async Task Handle_Received_Application_Message() public static async Task Handle_Received_Application_Message()
{ {
/* /*
@@ -42,11 +45,7 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter( .WithTopicTemplate(sampleTemplate.WithParameter("id", "2"))
f =>
{
f.WithTopic("mqttnet/samples/topic/2");
})
.Build(); .Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -88,11 +87,8 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter( .WithTopicTemplate(
f => sampleTemplate.WithParameter("id", "1"))
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build(); .Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -121,21 +117,12 @@ public static class Client_Subscribe_Samples
// Create the subscribe options including several topics with different options. // Create the subscribe options including several topics with different options.
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic. // It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter( .WithTopicTemplate(
f => sampleTemplate.WithParameter("id", "1"))
{ .WithTopicTemplate(
f.WithTopic("mqttnet/samples/topic/1"); sampleTemplate.WithParameter("id", "2"), noLocal: true)
}) .WithTopicTemplate(
.WithTopicFilter( sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
f =>
{
f.WithTopic("mqttnet/samples/topic/2").WithNoLocal();
})
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/3").WithRetainHandling(MqttRetainHandling.SendAtSubscribe);
})
.Build(); .Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -162,11 +149,7 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter( .WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build(); .Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -184,7 +167,7 @@ public static class Client_Subscribe_Samples
* This sample shows how to achieve concurrent processing and not have message AutoAcknowledged * This sample shows how to achieve concurrent processing and not have message AutoAcknowledged
* This to have a proper QoS1 (at-least-once) experience for what at least MQTT specification can provide * This to have a proper QoS1 (at-least-once) experience for what at least MQTT specification can provide
*/ */
mqttClient.ApplicationMessageReceivedAsync += async ea => mqttClient.ApplicationMessageReceivedAsync += ea =>
{ {
ea.AutoAcknowledge = false; ea.AutoAcknowledge = false;
@@ -201,6 +184,8 @@ public static class Client_Subscribe_Samples
} }
_ = Task.Run(ProcessAsync, shutdownToken); _ = Task.Run(ProcessAsync, shutdownToken);
return Task.CompletedTask;
}; };
} }
+2 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -17,6 +17,7 @@
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" /> <ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" /> <ProjectReference Include="..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" /> <ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" /> <ProjectReference Include="..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj" /> <ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,87 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
namespace MQTTnet.Samples.ManagedClient;
public sealed class Managed_Client_Subscribe_Samples
{
public static async Task Connect_Client()
{
/*
* This sample creates a simple managed MQTT client and connects to a public broker, subscribe to a topic and verifies subscription result.
*
* The managed client extends the existing _MqttClient_. It adds the following features.
* - Reconnecting when connection is lost.
* - Storing pending messages in an internal queue so that an enqueue is possible while the client remains not connected.
*/
var mqttFactory = new MqttFactory();
var subscribed = false;
using (var managedMqttClient = mqttFactory.CreateManagedMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
.Build();
var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder()
.WithClientOptions(mqttClientOptions)
.Build();
await managedMqttClient.StartAsync(managedMqttClientOptions);
// The application message is not sent. It is stored in an internal queue and
// will be sent when the client is connected.
await managedMqttClient.EnqueueAsync("Topic", "Payload");
Console.WriteLine("The managed MQTT client is connected.");
// Wait until the queue is fully processed.
SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000);
Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}");
managedMqttClient.SubscriptionsChangedAsync += args => SubscriptionsResultAsync(args, ref subscribed);
await managedMqttClient.SubscribeAsync("Topic").ConfigureAwait(false);
SpinWait.SpinUntil(() => subscribed, 1000);
Console.WriteLine($"Subscription properly done {subscribed}");
}
}
private static Task SubscriptionsResultAsync(SubscriptionsChangedEventArgs arg, ref bool subscribed)
{
foreach (var mqttClientSubscribeResult in arg.SubscribeResult)
{
Console.WriteLine($"Subscription reason {mqttClientSubscribeResult.ReasonString}");
foreach (var item in mqttClientSubscribeResult.Items)
{
Console.WriteLine($"For topic filter {item.TopicFilter}, result code: {item.ResultCode}");
if (item.TopicFilter.Topic == "Topic" && item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS0 && !subscribed)
{
subscribed = true;
}
}
}
foreach (var mqttClientUnsubscribeResult in arg.UnsubscribeResult)
{
Console.WriteLine($"Unsubscription reason {mqttClientUnsubscribeResult.ReasonString}");
foreach (var item in mqttClientUnsubscribeResult.Items)
{
Console.WriteLine($"For topic filter {item.TopicFilter}, result code: {item.ResultCode}");
}
}
return Task.CompletedTask;
}
}
@@ -48,7 +48,6 @@ namespace MQTTnet.AspNetCore.Tests
{ {
var mockup = new ConnectionHandlerMockup(); var mockup = new ConnectionHandlerMockup();
using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx)))) using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx))))
.UseStartup<Startup>() .UseStartup<Startup>()
.ConfigureServices( .ConfigureServices(
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -6,7 +6,6 @@ using MQTTnet.Adapter;
using MQTTnet.AspNetCore.Client.Tcp; using MQTTnet.AspNetCore.Client.Tcp;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using System; using System;
using System.Net;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
@@ -22,8 +21,7 @@ namespace MQTTnet.AspNetCore.Client
{ {
case MqttClientTcpOptions tcpOptions: case MqttClientTcpOptions tcpOptions:
{ {
var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort()); var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint);
var tcpConnection = new TcpConnection(endpoint);
var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)); var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535));
return new MqttConnectionContext(formatter, tcpConnection); return new MqttConnectionContext(formatter, tcpConnection);
@@ -15,15 +15,15 @@ using System.Threading.Tasks;
namespace MQTTnet.AspNetCore.Client.Tcp namespace MQTTnet.AspNetCore.Client.Tcp
{ {
public class TcpConnection : ConnectionContext public sealed class SocketConnection : ConnectionContext
{ {
private volatile bool _aborted; volatile bool _aborted;
private readonly EndPoint _endPoint; readonly EndPoint _endPoint;
private SocketSender _sender; SocketSender _sender;
private SocketReceiver _receiver; SocketReceiver _receiver;
private Socket _socket; Socket _socket;
private IDuplexPipe _application; IDuplexPipe _application;
public bool IsConnected { get; private set; } public bool IsConnected { get; private set; }
public override string ConnectionId { get; set; } public override string ConnectionId { get; set; }
@@ -31,12 +31,12 @@ namespace MQTTnet.AspNetCore.Client.Tcp
public override IDictionary<object, object> Items { get; set; } public override IDictionary<object, object> Items { get; set; }
public override IDuplexPipe Transport { get; set; } public override IDuplexPipe Transport { get; set; }
public TcpConnection(EndPoint endPoint) public SocketConnection(EndPoint endPoint)
{ {
_endPoint = endPoint; _endPoint = endPoint;
} }
public TcpConnection(Socket socket) public SocketConnection(Socket socket)
{ {
_socket = socket; _socket = socket;
_endPoint = socket.RemoteEndPoint; _endPoint = socket.RemoteEndPoint;
@@ -87,7 +87,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp
IsConnected = true; IsConnected = true;
} }
private async Task ExecuteAsync() async Task ExecuteAsync()
{ {
Exception sendError = null; Exception sendError = null;
try try
@@ -114,7 +114,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Unexpected exception in {nameof(TcpConnection)}.{nameof(StartAsync)}: " + ex); Console.WriteLine($"Unexpected exception in {nameof(SocketConnection)}.{nameof(StartAsync)}: " + ex);
} }
finally finally
{ {
@@ -122,7 +122,8 @@ namespace MQTTnet.AspNetCore.Client.Tcp
_application.Input.Complete(sendError); _application.Input.Complete(sendError);
} }
} }
private async Task DoReceive()
async Task DoReceive()
{ {
Exception error = null; Exception error = null;
@@ -171,7 +172,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp
} }
} }
private async Task ProcessReceives() async Task ProcessReceives()
{ {
while (true) while (true)
{ {
@@ -204,12 +205,12 @@ namespace MQTTnet.AspNetCore.Client.Tcp
} }
} }
private Exception ConnectionAborted() Exception ConnectionAborted()
{ {
return new MqttCommunicationException("Connection Aborted"); return new MqttCommunicationException("Connection Aborted");
} }
private async Task<Exception> DoSend() async Task<Exception> DoSend()
{ {
Exception error = null; Exception error = null;
@@ -240,7 +241,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp
return error; return error;
} }
private async Task ProcessSends() async Task ProcessSends()
{ {
while (true) while (true)
{ {
@@ -42,7 +42,7 @@
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
@@ -51,11 +51,11 @@
<DefineConstants>RELEASE;NETSTANDARD2_0</DefineConstants> <DefineConstants>RELEASE;NETSTANDARD2_0</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' "> <ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0' and '$(TargetFramework)' != 'netcoreapp2.1' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1' and '$(TargetFramework)' != 'net5.0' and '$(TargetFramework)' != 'net6.0' and '$(TargetFramework)' != 'net7.0' "> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp2.1' ">
<PackageReference Include="Microsoft.AspNetCore.Http.Connections" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Connections" Version="1.1.0" />
</ItemGroup> </ItemGroup>
@@ -33,7 +33,7 @@ namespace MQTTnet.AspNetCore
PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter)); PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter));
_connection = connection ?? throw new ArgumentNullException(nameof(connection)); _connection = connection ?? throw new ArgumentNullException(nameof(connection));
if (!(_connection is TcpConnection tcp) || tcp.IsConnected) if (!(_connection is SocketConnection tcp) || tcp.IsConnected)
{ {
_input = connection.Transport.Input; _input = connection.Transport.Input;
_output = connection.Transport.Output; _output = connection.Transport.Output;
@@ -83,8 +83,6 @@ namespace MQTTnet.AspNetCore
} }
} }
public bool IsReadingPacket { get; private set; }
public bool IsSecureConnection public bool IsSecureConnection
{ {
get get
@@ -111,7 +109,7 @@ namespace MQTTnet.AspNetCore
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
{ {
if (_connection is TcpConnection tcp && !tcp.IsConnected) if (_connection is SocketConnection tcp && !tcp.IsConnected)
{ {
await tcp.StartAsync().ConfigureAwait(false); await tcp.StartAsync().ConfigureAwait(false);
} }
@@ -164,11 +162,6 @@ namespace MQTTnet.AspNetCore
BytesReceived += received; BytesReceived += received;
return packet; return packet;
} }
else
{
// we did receive something but the message is not yet complete
IsReadingPacket = true;
}
} }
else if (readResult.IsCompleted) else if (readResult.IsCompleted)
{ {
@@ -189,12 +182,9 @@ namespace MQTTnet.AspNetCore
// completing the channel makes sure that there is no more data read after a protocol error // completing the channel makes sure that there is no more data read after a protocol error
_input?.Complete(exception); _input?.Complete(exception);
_output?.Complete(exception); _output?.Complete(exception);
throw; throw;
} }
finally
{
IsReadingPacket = false;
}
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
return null; return null;
+40 -7
View File
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,20 +15,52 @@ namespace MQTTnet.AspNetCore
{ {
public sealed class MqttHostedServer : MqttServer, IHostedService public sealed class MqttHostedServer : MqttServer, IHostedService
{ {
public MqttHostedServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) readonly MqttFactory _mqttFactory;
: base(options, adapters, logger) #if NETCOREAPP3_1_OR_GREATER
readonly IHostApplicationLifetime _hostApplicationLifetime;
public MqttHostedServer(IHostApplicationLifetime hostApplicationLifetime, MqttFactory mqttFactory,
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
options,
adapters,
logger)
{ {
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
_hostApplicationLifetime = hostApplicationLifetime;
} }
#else
public Task StartAsync(CancellationToken cancellationToken) public MqttHostedServer(MqttFactory mqttFactory,
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
options,
adapters,
logger)
{ {
_ = StartAsync(); _mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
return Task.CompletedTask; }
#endif
public async Task StartAsync(CancellationToken cancellationToken)
{
// The yield makes sure that the hosted service is considered up and running.
await Task.Yield();
#if NETCOREAPP3_1_OR_GREATER
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
#else
_ = StartAsync();
#endif
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
return StopAsync(); return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
} }
#if NETCOREAPP3_1_OR_GREATER
private void OnStarted()
{
_ = StartAsync();
}
#endif
} }
} }
@@ -4,6 +4,7 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using MQTTnet.Adapter; using MQTTnet.Adapter;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
@@ -14,86 +15,71 @@ namespace MQTTnet.AspNetCore
{ {
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddMqttServer(this IServiceCollection serviceCollection, Action<MqttServerOptionsBuilder> configure = null)
{
if (serviceCollection is null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}
serviceCollection.AddMqttConnectionHandler();
serviceCollection.AddHostedMqttServer(configure);
return serviceCollection;
}
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, MqttServerOptions options) public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, MqttServerOptions options)
{ {
if (options == null) throw new ArgumentNullException(nameof(options)); if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
services.AddSingleton(options); services.AddSingleton(options);
services.AddHostedMqttServer(); services.AddHostedMqttServer();
return services; return services;
} }
public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action<MqttServerOptionsBuilder> configure = null) public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action<MqttServerOptionsBuilder> configure)
{ {
services.AddSingleton(s => if (services == null)
{ {
var serverOptionsBuilder = new MqttServerOptionsBuilder(); throw new ArgumentNullException(nameof(services));
configure?.Invoke(serverOptionsBuilder); }
return serverOptionsBuilder.Build();
});
services.AddHostedMqttServer(); var serverOptionsBuilder = new MqttServerOptionsBuilder();
return services; configure?.Invoke(serverOptionsBuilder);
var options = serverOptionsBuilder.Build();
return AddHostedMqttServer(services, options);
}
public static void AddHostedMqttServer(this IServiceCollection services)
{
// The user may have these services already registered.
services.TryAddSingleton<IMqttNetLogger>(MqttNetNullLogger.Instance);
services.TryAddSingleton(new MqttFactory());
services.AddSingleton<MqttHostedServer>();
services.AddSingleton<IHostedService>(s => s.GetService<MqttHostedServer>());
services.AddSingleton<MqttServer>(s => s.GetService<MqttHostedServer>());
} }
public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action<AspNetMqttServerOptionsBuilder> configure) public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action<AspNetMqttServerOptionsBuilder> configure)
{ {
services.AddSingleton(s => if (services == null)
{ {
var builder = new AspNetMqttServerOptionsBuilder(s); throw new ArgumentNullException(nameof(services));
configure(builder); }
return builder.Build();
}); services.AddSingleton(
s =>
{
var builder = new AspNetMqttServerOptionsBuilder(s);
configure(builder);
return builder.Build();
});
services.AddHostedMqttServer(); services.AddHostedMqttServer();
return services; return services;
} }
static IServiceCollection AddHostedMqttServer(this IServiceCollection services)
{
var logger = new MqttNetEventLogger();
services.AddSingleton<IMqttNetLogger>(logger);
services.AddSingleton<MqttHostedServer>();
services.AddSingleton<IHostedService>(s => s.GetService<MqttHostedServer>());
services.AddSingleton<MqttServer>(s => s.GetService<MqttHostedServer>());
return services;
}
public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttWebSocketServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttWebSocketServerAdapter>());
return services;
}
public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttTcpServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttTcpServerAdapter>());
return services;
}
public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services) public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services)
{ {
services.AddSingleton<MqttConnectionHandler>(); services.AddSingleton<MqttConnectionHandler>();
@@ -101,5 +87,44 @@ namespace MQTTnet.AspNetCore
return services; return services;
} }
public static void AddMqttLogger(this IServiceCollection services, IMqttNetLogger logger)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton(logger);
}
public static IServiceCollection AddMqttServer(this IServiceCollection serviceCollection, Action<MqttServerOptionsBuilder> configure = null)
{
if (serviceCollection is null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}
serviceCollection.AddMqttConnectionHandler();
serviceCollection.AddHostedMqttServer(configure);
return serviceCollection;
}
public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttTcpServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttTcpServerAdapter>());
return services;
}
public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services)
{
services.AddSingleton<MqttWebSocketServerAdapter>();
services.AddSingleton<IMqttServerAdapter>(s => s.GetService<MqttWebSocketServerAdapter>());
return services;
}
} }
} }
@@ -29,12 +29,12 @@ namespace MQTTnet.Benchmarks
var task = Task.Run(() => server.AcceptSocket()); var task = Task.Run(() => server.AcceptSocket());
var clientConnection = new TcpConnection(new IPEndPoint(IPAddress.Loopback, 1883)); var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883));
clientConnection.StartAsync().GetAwaiter().GetResult(); clientConnection.StartAsync().GetAwaiter().GetResult();
_client = clientConnection.Transport; _client = clientConnection.Transport;
var serverConnection = new TcpConnection(task.GetAwaiter().GetResult()); var serverConnection = new SocketConnection(task.GetAwaiter().GetResult());
serverConnection.StartAsync().GetAwaiter().GetResult(); serverConnection.StartAsync().GetAwaiter().GetResult();
_server = serverConnection.Transport; _server = serverConnection.Transport;
} }
@@ -25,6 +25,8 @@ namespace MQTTnet.Extensions.ManagedClient
event Func<ManagedProcessFailedEventArgs, Task> SynchronizingSubscriptionsFailedAsync; event Func<ManagedProcessFailedEventArgs, Task> SynchronizingSubscriptionsFailedAsync;
event Func<SubscriptionsChangedEventArgs, Task> SubscriptionsChangedAsync;
IMqttClient InternalClient { get; } IMqttClient InternalClient { get; }
bool IsConnected { get; } bool IsConnected { get; }
@@ -45,8 +47,8 @@ namespace MQTTnet.Extensions.ManagedClient
Task StopAsync(bool cleanDisconnect = true); Task StopAsync(bool cleanDisconnect = true);
Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters); Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters);
Task UnsubscribeAsync(ICollection<string> topics); Task UnsubscribeAsync(IEnumerable<string> topics);
} }
} }
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class InterceptingPublishMessageEventArgs : EventArgs
{
public InterceptingPublishMessageEventArgs(ManagedMqttApplicationMessage applicationMessage)
{
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
}
public ManagedMqttApplicationMessage ApplicationMessage { get; }
public bool AcceptPublish { get; set; } = true;
}
}
@@ -19,15 +19,17 @@ namespace MQTTnet.Extensions.ManagedClient
{ {
public sealed class ManagedMqttClient : Disposable, IManagedMqttClient public sealed class ManagedMqttClient : Disposable, IManagedMqttClient
{ {
readonly MqttNetSourceLogger _logger;
readonly AsyncEvent<InterceptingPublishMessageEventArgs> _interceptingPublishMessageEvent = new AsyncEvent<InterceptingPublishMessageEventArgs>();
readonly AsyncEvent<ApplicationMessageProcessedEventArgs> _applicationMessageProcessedEvent = new AsyncEvent<ApplicationMessageProcessedEventArgs>(); readonly AsyncEvent<ApplicationMessageProcessedEventArgs> _applicationMessageProcessedEvent = new AsyncEvent<ApplicationMessageProcessedEventArgs>();
readonly AsyncEvent<ApplicationMessageSkippedEventArgs> _applicationMessageSkippedEvent = new AsyncEvent<ApplicationMessageSkippedEventArgs>(); readonly AsyncEvent<ApplicationMessageSkippedEventArgs> _applicationMessageSkippedEvent = new AsyncEvent<ApplicationMessageSkippedEventArgs>();
readonly AsyncEvent<ConnectingFailedEventArgs> _connectingFailedEvent = new AsyncEvent<ConnectingFailedEventArgs>(); readonly AsyncEvent<ConnectingFailedEventArgs> _connectingFailedEvent = new AsyncEvent<ConnectingFailedEventArgs>();
readonly AsyncEvent<EventArgs> _connectionStateChangedEvent = new AsyncEvent<EventArgs>(); readonly AsyncEvent<EventArgs> _connectionStateChangedEvent = new AsyncEvent<EventArgs>();
readonly AsyncEvent<ManagedProcessFailedEventArgs> _synchronizingSubscriptionsFailedEvent = new AsyncEvent<ManagedProcessFailedEventArgs>(); readonly AsyncEvent<ManagedProcessFailedEventArgs> _synchronizingSubscriptionsFailedEvent = new AsyncEvent<ManagedProcessFailedEventArgs>();
readonly AsyncEvent<SubscriptionsChangedEventArgs> _subscriptionsChangedEvent = new AsyncEvent<SubscriptionsChangedEventArgs>();
readonly MqttNetSourceLogger _logger;
readonly BlockingQueue<ManagedMqttApplicationMessage> _messageQueue = new BlockingQueue<ManagedMqttApplicationMessage>();
readonly BlockingQueue<ManagedMqttApplicationMessage> _messageQueue = new BlockingQueue<ManagedMqttApplicationMessage>();
readonly AsyncLock _messageQueueLock = new AsyncLock(); readonly AsyncLock _messageQueueLock = new AsyncLock();
/// <summary> /// <summary>
@@ -81,13 +83,19 @@ namespace MQTTnet.Extensions.ManagedClient
add => _applicationMessageSkippedEvent.AddHandler(value); add => _applicationMessageSkippedEvent.AddHandler(value);
remove => _applicationMessageSkippedEvent.RemoveHandler(value); remove => _applicationMessageSkippedEvent.RemoveHandler(value);
} }
public event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync public event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync
{ {
add => _applicationMessageProcessedEvent.AddHandler(value); add => _applicationMessageProcessedEvent.AddHandler(value);
remove => _applicationMessageProcessedEvent.RemoveHandler(value); remove => _applicationMessageProcessedEvent.RemoveHandler(value);
} }
public event Func<InterceptingPublishMessageEventArgs, Task> InterceptPublishMessageAsync
{
add => _interceptingPublishMessageEvent.AddHandler(value);
remove => _interceptingPublishMessageEvent.RemoveHandler(value);
}
public event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync public event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync
{ {
add => InternalClient.ApplicationMessageReceivedAsync += value; add => InternalClient.ApplicationMessageReceivedAsync += value;
@@ -124,6 +132,12 @@ namespace MQTTnet.Extensions.ManagedClient
remove => _synchronizingSubscriptionsFailedEvent.RemoveHandler(value); remove => _synchronizingSubscriptionsFailedEvent.RemoveHandler(value);
} }
public event Func<SubscriptionsChangedEventArgs, Task> SubscriptionsChangedAsync
{
add => _subscriptionsChangedEvent.AddHandler(value);
remove => _subscriptionsChangedEvent.RemoveHandler(value);
}
public IMqttClient InternalClient { get; } public IMqttClient InternalClient { get; }
public bool IsConnected => InternalClient.IsConnected; public bool IsConnected => InternalClient.IsConnected;
@@ -133,7 +147,7 @@ namespace MQTTnet.Extensions.ManagedClient
public ManagedMqttClientOptions Options { get; private set; } public ManagedMqttClientOptions Options { get; private set; }
public int PendingApplicationMessagesCount => _messageQueue.Count; public int PendingApplicationMessagesCount => _messageQueue.Count;
public async Task EnqueueAsync(MqttApplicationMessage applicationMessage) public async Task EnqueueAsync(MqttApplicationMessage applicationMessage)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -261,7 +275,7 @@ namespace MQTTnet.Extensions.ManagedClient
ThrowIfDisposed(); ThrowIfDisposed();
_isCleanDisconnect = cleanDisconnect; _isCleanDisconnect = cleanDisconnect;
StopPublishing(); StopPublishing();
StopMaintainingConnection(); StopMaintainingConnection();
@@ -274,7 +288,7 @@ namespace MQTTnet.Extensions.ManagedClient
} }
} }
public Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters) public Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -302,7 +316,7 @@ namespace MQTTnet.Extensions.ManagedClient
return CompletedTask.Instance; return CompletedTask.Instance;
} }
public Task UnsubscribeAsync(ICollection<string> topics) public Task UnsubscribeAsync(IEnumerable<string> topics)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -353,6 +367,13 @@ namespace MQTTnet.Extensions.ManagedClient
return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime; return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime;
} }
CancellationTokenSource NewTimeoutToken(CancellationToken linkedToken)
{
var newTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource(linkedToken);
newTimeoutToken.CancelAfter(Options.ClientOptions.Timeout);
return newTimeoutToken;
}
async Task HandleSubscriptionExceptionAsync(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions) async Task HandleSubscriptionExceptionAsync(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{ {
_logger.Warning(exception, "Synchronizing subscriptions failed."); _logger.Warning(exception, "Synchronizing subscriptions failed.");
@@ -363,6 +384,14 @@ namespace MQTTnet.Extensions.ManagedClient
} }
} }
async Task HandleSubscriptionsResultAsync(SendSubscribeUnsubscribeResult subscribeUnsubscribeResult)
{
if (_subscriptionsChangedEvent.HasHandlers)
{
await _subscriptionsChangedEvent.InvokeAsync(new SubscriptionsChangedEventArgs(subscribeUnsubscribeResult.SubscribeResults, subscribeUnsubscribeResult.UnsubscribeResults)).ConfigureAwait(false);
}
}
async Task MaintainConnectionAsync(CancellationToken cancellationToken) async Task MaintainConnectionAsync(CancellationToken cancellationToken)
{ {
try try
@@ -387,7 +416,7 @@ namespace MQTTnet.Extensions.ManagedClient
{ {
if (_isCleanDisconnect) if (_isCleanDisconnect)
{ {
using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) using (var disconnectTimeout = NewTimeoutToken(CancellationToken.None))
{ {
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false); await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false);
} }
@@ -437,7 +466,7 @@ namespace MQTTnet.Extensions.ManagedClient
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
await TryPublishQueuedMessageAsync(message).ConfigureAwait(false); await TryPublishQueuedMessageAsync(message, cancellationToken).ConfigureAwait(false);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -453,7 +482,7 @@ namespace MQTTnet.Extensions.ManagedClient
} }
} }
async Task PublishReconnectSubscriptionsAsync() async Task PublishReconnectSubscriptionsAsync(CancellationToken cancellationToken)
{ {
_logger.Info("Publishing subscriptions at reconnect"); _logger.Info("Publishing subscriptions at reconnect");
@@ -464,6 +493,7 @@ namespace MQTTnet.Extensions.ManagedClient
if (_reconnectSubscriptions.Any()) if (_reconnectSubscriptions.Any())
{ {
topicFilters = new List<MqttTopicFilter>(); topicFilters = new List<MqttTopicFilter>();
SendSubscribeUnsubscribeResult subscribeUnsubscribeResult;
foreach (var sub in _reconnectSubscriptions) foreach (var sub in _reconnectSubscriptions)
{ {
@@ -471,12 +501,14 @@ namespace MQTTnet.Extensions.ManagedClient
if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{ {
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
topicFilters.Clear(); topicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
} }
} }
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
} }
} }
catch (Exception exception) catch (Exception exception)
@@ -492,6 +524,7 @@ namespace MQTTnet.Extensions.ManagedClient
while (await _subscriptionsQueuedSignal.WaitAsync(GetRemainingTime(endTime), cancellationToken).ConfigureAwait(false)) while (await _subscriptionsQueuedSignal.WaitAsync(GetRemainingTime(endTime), cancellationToken).ConfigureAwait(false))
{ {
List<MqttTopicFilter> subscriptions; List<MqttTopicFilter> subscriptions;
SendSubscribeUnsubscribeResult subscribeUnsubscribeResult;
HashSet<string> unsubscriptions; HashSet<string> unsubscriptions;
lock (_subscriptions) lock (_subscriptions)
@@ -527,12 +560,14 @@ namespace MQTTnet.Extensions.ManagedClient
if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{ {
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
addedTopicFilters.Clear(); addedTopicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
} }
} }
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
var removedTopicFilters = new List<string>(); var removedTopicFilters = new List<string>();
foreach (var unSub in unsubscriptions) foreach (var unSub in unsubscriptions)
@@ -541,12 +576,14 @@ namespace MQTTnet.Extensions.ManagedClient
if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{ {
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
removedTopicFilters.Clear(); removedTopicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
} }
} }
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false); subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
} }
} }
@@ -560,7 +597,7 @@ namespace MQTTnet.Extensions.ManagedClient
MqttClientConnectResult connectResult = null; MqttClientConnectResult connectResult = null;
try try
{ {
using (var connectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) using (var connectTimeout = NewTimeoutToken(cancellationToken))
{ {
connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false); connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false);
} }
@@ -579,8 +616,10 @@ namespace MQTTnet.Extensions.ManagedClient
} }
} }
async Task SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions) async Task<SendSubscribeUnsubscribeResult> SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions, CancellationToken cancellationToken)
{ {
var subscribeResults = new List<MqttClientSubscribeResult>();
var unsubscribeResults = new List<MqttClientUnsubscribeResult>();
try try
{ {
if (removedSubscriptions != null && removedSubscriptions.Any()) if (removedSubscriptions != null && removedSubscriptions.Any())
@@ -592,7 +631,11 @@ namespace MQTTnet.Extensions.ManagedClient
unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription); unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription);
} }
await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false); using (var unsubscribeTimeout = NewTimeoutToken(cancellationToken))
{
var unsubscribeResult = await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build(), unsubscribeTimeout.Token).ConfigureAwait(false);
unsubscribeResults.Add(unsubscribeResult);
}
//clear because these worked, maybe the subscribe below will fail, only report those //clear because these worked, maybe the subscribe below will fail, only report those
removedSubscriptions.Clear(); removedSubscriptions.Clear();
@@ -607,13 +650,19 @@ namespace MQTTnet.Extensions.ManagedClient
subscribeOptionsBuilder.WithTopicFilter(addedSubscription); subscribeOptionsBuilder.WithTopicFilter(addedSubscription);
} }
await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build()).ConfigureAwait(false); using (var subscribeTimeout = NewTimeoutToken(cancellationToken))
{
var subscribeResult = await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build(), subscribeTimeout.Token).ConfigureAwait(false);
subscribeResults.Add(subscribeResult);
}
} }
} }
catch (Exception exception) catch (Exception exception)
{ {
await HandleSubscriptionExceptionAsync(exception, addedSubscriptions, removedSubscriptions).ConfigureAwait(false); await HandleSubscriptionExceptionAsync(exception, addedSubscriptions, removedSubscriptions).ConfigureAwait(false);
} }
return new SendSubscribeUnsubscribeResult(subscribeResults, unsubscribeResults);
} }
void StartPublishing() void StartPublishing()
@@ -659,7 +708,7 @@ namespace MQTTnet.Extensions.ManagedClient
{ {
var oldConnectionState = InternalClient.IsConnected; var oldConnectionState = InternalClient.IsConnected;
var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false); var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false);
if (connectionState == ReconnectionResult.NotConnected) if (connectionState == ReconnectionResult.NotConnected)
{ {
StopPublishing(); StopPublishing();
@@ -667,7 +716,7 @@ namespace MQTTnet.Extensions.ManagedClient
} }
else if (connectionState == ReconnectionResult.Reconnected) else if (connectionState == ReconnectionResult.Reconnected)
{ {
await PublishReconnectSubscriptionsAsync().ConfigureAwait(false); await PublishReconnectSubscriptionsAsync(cancellationToken).ConfigureAwait(false);
StartPublishing(); StartPublishing();
} }
else if (connectionState == ReconnectionResult.Recovered) else if (connectionState == ReconnectionResult.Recovered)
@@ -697,12 +746,26 @@ namespace MQTTnet.Extensions.ManagedClient
} }
} }
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message) async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message, CancellationToken cancellationToken)
{ {
Exception transmitException = null; Exception transmitException = null;
bool acceptPublish = true;
try try
{ {
await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false); if (_interceptingPublishMessageEvent.HasHandlers)
{
var interceptEventArgs = new InterceptingPublishMessageEventArgs(message);
await _interceptingPublishMessageEvent.InvokeAsync(interceptEventArgs).ConfigureAwait(false);
acceptPublish = interceptEventArgs.AcceptPublish;
}
if (acceptPublish)
{
using (var publishTimeout = NewTimeoutToken(cancellationToken))
{
await InternalClient.PublishAsync(message.ApplicationMessage, publishTimeout.Token).ConfigureAwait(false);
}
}
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync
{ {
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using MQTTnet.Client;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class SendSubscribeUnsubscribeResult
{
public SendSubscribeUnsubscribeResult(List<MqttClientSubscribeResult> subscribeResults, List<MqttClientUnsubscribeResult> unsubscribeResults)
{
SubscribeResults = subscribeResults ?? throw new ArgumentNullException(nameof(subscribeResults));
UnsubscribeResults = unsubscribeResults ?? throw new ArgumentNullException(nameof(unsubscribeResults));
}
public List<MqttClientSubscribeResult> SubscribeResults { get; private set; }
public List<MqttClientUnsubscribeResult> UnsubscribeResults { get; private set; }
}
}
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Client;
using System;
using System.Collections.Generic;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class SubscriptionsChangedEventArgs : EventArgs
{
public SubscriptionsChangedEventArgs(List<MqttClientSubscribeResult> subscribeResult, List<MqttClientUnsubscribeResult> unsubscribeResult)
{
SubscribeResult = subscribeResult ?? throw new ArgumentNullException(nameof(subscribeResult));
UnsubscribeResult = unsubscribeResult ?? throw new ArgumentNullException(nameof(unsubscribeResult));
}
public List<MqttClientSubscribeResult> SubscribeResult { get; }
public List<MqttClientUnsubscribeResult> UnsubscribeResult { get; }
}
}
@@ -1,3 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461;net48</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
<AssemblyName>MQTTnet.Extensions.TopicTemplate</AssemblyName>
<RootNamespace>MQTTnet.Extensions.TopicTemplate</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Company>The contributors of MQTTnet</Company>
<Product>MQTTnet</Product>
<Description>
Provides mqtt topic templating logic to support dispatch,
routing and similar functionality based on the well known moustache syntax (AsyncAPI compatible).
</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>The contributors of MQTTnet</Authors>
<PackageId>MQTTnet.Extensions.TopicTemplate</PackageId>
<SignAssembly>false</SignAssembly>
<DelaySign>false</DelaySign>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>MQTT Message Queue Telemetry MQTTClient Messaging Routing AsyncAPI Template</PackageTags>
<NeutralLanguage>en-US</NeutralLanguage>
<PackageIcon>nuget.png</PackageIcon>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
<DefaultLanguage>en</DefaultLanguage>
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
</PropertyGroup>
<ItemGroup>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\Images\nuget.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,357 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate
{
/// <summary>
/// A topic template is an MQTT topic filter string that may contain
/// segments in curly braces called parameters. This well-known
/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
/// The topic template is designed to support dynamic subscription/publication,
/// message-topic matching and routing. It is intended to be more safe and
/// convenient than String.Format() for aforementioned purposes.
/// </summary>
/// <example>
/// topic/subtopic/{parameter}/{otherParameter}
/// </example>
public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
{
static readonly Regex MoustacheRegex = new Regex("{([^/]+?)}", RegexOptions.Compiled);
readonly string[] _parameterSegments;
string _topicFilter;
/// <summary>
/// Create a topic template from an mqtt topic filter with moustache placeholders.
/// </summary>
/// <param
/// name="topicTemplate">
/// </param>
/// <exception
/// cref="ArgumentNullException">
/// </exception>
public MqttTopicTemplate(string topicTemplate)
{
if (topicTemplate == null)
{
throw new ArgumentNullException(nameof(topicTemplate));
}
MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
Template = topicTemplate;
_parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
.Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
.Select(s => s.Length > 0 ? s : null)
.ToArray();
}
/// <summary>
/// Yield the template parameter names.
/// </summary>
public IEnumerable<string> Parameters => _parameterSegments.Where(s => s != null);
/// <summary>
/// The topic template string representation, e.g. A/B/{foo}/D.
/// </summary>
public string Template { get; }
/// <summary>
/// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
/// ends with a multi-level wildcard (hash), this will be reflected here.
/// </summary>
public string TopicFilter
{
get
{
LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
return _topicFilter;
}
}
/// <summary>
/// Return the topic filter of this template, ending with a multi-level wildcard (hash).
/// </summary>
public string TopicTreeRootFilter
{
get
{
var filter = TopicFilter;
// append slash if neccessary
if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.LevelSeparator;
}
// append hash if neccessary
if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.MultiLevelWildcard;
}
return filter;
}
}
public bool Equals(MqttTopicTemplate other)
{
return other != null && Template == other.Template;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((MqttTopicTemplate)obj);
}
/// <summary>
/// Determine the shortest common prefix of the given templates. Partial segments
/// are not returned.
/// </summary>
/// <param
/// name="templates">
/// topic templates
/// </param>
/// <returns></returns>
public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable<MqttTopicTemplate> templates)
{
string root = null;
string CommonPrefix(string a, string b)
{
var maxIndex = Math.Min(a.Length, b.Length) - 1;
for (var i = 0; i <= maxIndex; i++)
{
if (a[i] != b[i])
{
return a.Substring(0, i);
}
}
return a.Substring(0, maxIndex+1);
}
foreach (string topic in from template in templates select template.Template)
{
root = root == null ? topic : CommonPrefix(root, topic);
}
if (string.IsNullOrEmpty(root))
return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
if (root.Contains(MqttTopicFilterComparer.LevelSeparator) &&
!root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
!root.EndsWith("}"))
{
root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator)+1);
}
if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
root += MqttTopicFilterComparer.SingleLevelWildcard;
return new MqttTopicTemplate(root);
}
public override int GetHashCode()
{
return Template.GetHashCode();
}
/// <summary>
/// Test if this topic template matches a given topic.
/// </summary>
/// <param
/// name="topic">
/// a fully specified topic
/// </param>
/// <param
/// name="subtree">
/// true to match including the subtree (multi-level wildcard)
/// </param>
/// <returns>true iff the topic matches the template's filter</returns>
/// <exception
/// cref="InvalidOperationException">
/// </exception>
/// <exception
/// cref="ArgumentException">
/// if the topic is invalid
/// </exception>
public bool MatchesTopic(string topic, bool subtree = false)
{
var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
{
throw new InvalidOperationException("Invalid filter");
}
if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
{
throw new ArgumentException("Invalid topic", nameof(topic));
}
return comparison == MqttTopicFilterCompareResult.IsMatch;
}
/// <summary>
/// Extract the parameter values from a topic corresponding to the template
/// parameters. The topic has to match this template.
/// </summary>
/// <param
/// name="topic">
/// the topic
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
{
if (!MatchesTopic(topic))
{
throw new ArgumentException("the topic has to match this template", nameof(topic));
}
return parseParameterValuesInternal(topic);
}
/// <summary>
/// Extract the parameter values from the message topic corresponding to the template
/// parameters. The message topic has to match this topic template.
/// </summary>
/// <param
/// name="message">
/// the message
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
{
return ParseParameterValues(message.Topic);
}
/// <summary>
/// Try to set a parameter to a given value. If the parameter is not present,
/// this is returned. The value must not contain slashes.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <returns></returns>
public MqttTopicTemplate TrySetParameter(string parameter, string value)
{
if (parameter != null && _parameterSegments.Contains(parameter))
{
return WithParameter(parameter, value);
}
return this;
}
/// <summary>
/// Replace the given parameter with a single-level wildcard (plus sign).
/// </summary>
/// <param
/// name="parameter">
/// parameter name
/// </param>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithoutParameter(string parameter)
{
if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
{
throw new ArgumentException("topic template parameter must exist.");
}
return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
}
/// <summary>
/// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
/// the method trows. The value must not contain slashes or wildcards.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <exception
/// cref="ArgumentException">
/// when the parameter is not present
/// </exception>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithParameter(string parameter, string value)
{
if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) ||
value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) ||
value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
{
throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
}
return ReplaceInternal(parameter, value);
}
private MqttTopicTemplate ReplaceInternal(string parameter, string value)
{
var moustache = "{" + parameter + "}";
return new MqttTopicTemplate(Template.Replace(moustache, value));
}
/// <summary>
/// Reuse parameters as they are extracted using another topic template on this template
/// when the parameter name matches. Useful
/// for compatibility routing.
/// </summary>
/// <param
/// name="parameters">
/// </param>
/// <returns></returns>
public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
{
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
}
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
{
// because we have a match, we know the segment array is at least the template's length
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
for (var i = 0; i < _parameterSegments.Length; i++)
{
var name = _parameterSegments[i];
if (name != null)
{
yield return (name, i, segments[i]);
}
}
}
}
}
@@ -0,0 +1,79 @@
# MQTTNet.Extensions.TopicTemplate
Provides mqtt topic templating logic to support dispatch, routing and
similar functionality based on the well known moustache syntax, aka
AsyncAPI dynamic channel address.
Generating and parsing MQTT topics and topic filters is often done in
an ad-hoc fashion, leading to error-prone code and subtly introduced
showstoppers like forgetting to strip a slash from a parameter. To
remedy, this extension aids you write safe, maintainable logic
for topic generation, routing and dispatch.
## How it works
The package lets you write MQTTNet code like:
```csharp
var template = new MqttTopicTemplate("App/v1/{sender}/message");
var filter = new MqttTopicFilterBuilder()
.WithTopicTemplate(template) // (1)
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
// filter.Topic == "App/v1/+/message"
var myTopic = template
.WithParameter("sender", "me, myself & i"); // (2)
var message = new MqttApplicationMessageBuilder()
.WithTopicTemplate( // (3)
myTopic)
.WithPayload("Hello!")
.Build();
// message.Topic == "App/v1/me, myself & i/message"
Assert.IsTrue(message.MatchesTopicTemplate(template)); // (4)
```
At (1), the parameter "sender" was automatically replaced by a plus sign
(single level wildcard) because the caller is creating a topic filter.
Using the same template (they are immutable), you can send
a message. At (2), we replace the parameter with a known value.
If that value contained a level separator or topic filter only
characters, we'd get an ArgumentException. If we forgot to replace
the parameter "sender" (e.g. when placing template not myTopic),
we would be prevented from sending at (3).
On the receiving side, a message can be matched to a template
using code like at (4).
These daily tasks become much safer and easier to code than
string.Format() and Split() allow for. More elaborate computation is supported
too. You could extract the sender using the template or route
messages based on topic content.
## Features
- Safe handling of topic parameters in moustache syntax
- Simplifies topic generation and parsing logic
- Match topics and extract the values of their parameters
- Translate between topic templates to ease dynamic routing
- Derive a canonical topic filter from a bunch of templates
- Integrates with MQTTNet message, subscription and filter builders
- Extension method friendly (sealed class with one public ctor)
## Additional documentation
- Some of the [unit tests](https://github.com/dotnet/MQTTnet/blob/master/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs) are instructive
- Some examples feature the extension, e.g. TopicGenerator, Client_Subscribe_Samples
## Feedback
Report any issues with the main project on https://github.com/dotnet/MQTTnet/issues
@@ -0,0 +1,190 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using MQTTnet.Client;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate
{
public static class TopicTemplateExtensions
{
/// <summary>
/// Modify this message builder to respond to a given message. The
/// message's response topic and correlation data are included
/// in the message builder.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="BuildResponse" />
public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
{
if (!string.IsNullOrEmpty(message.ResponseTopic))
{
throw new ArgumentException("message does not have a response topic");
}
return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
}
/// <summary>
/// Set the filter topic according to the template, with
/// remaining template parameters substituted by single-level
/// wildcard.
/// </summary>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
}
/// <summary>
/// Create a message builder from this template. The template must not have
/// remaining parameters.
/// </summary>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>a new message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
{
return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
}
/// <summary>
/// Return a message builder to respond to this message. The
/// message's response topic and correlation data are included
/// in the response message builder.
/// </summary>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="AsResponseTo" />
public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
{
return new MqttApplicationMessageBuilder().AsResponseTo(message);
}
/// <summary>
/// Return whether the message matches the given topic template.
/// </summary>
/// <param
/// name="message">
/// a message
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subtree">
/// whether to include the topic subtree
/// </param>
/// <returns></returns>
public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
{
return topicTemplate.MatchesTopic(message.Topic, subtree);
}
/// <summary>
/// Set the filter topic according to the template, with
/// template parameters substituted by a single-level
/// wildcard.
/// </summary>
/// <param
/// name="builder">
/// a filter builder
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
}
/// <summary>
/// Set the subscription to the template's topic filter.
/// </summary>
/// <returns>the builder</returns>
public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
this MqttClientSubscribeOptionsBuilder builder,
MqttTopicTemplate topicTemplate,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
bool noLocal = false,
bool retainAsPublished = false,
MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
{
return builder.WithTopicFilter(
new MqttTopicFilter
{
Topic = topicTemplate.TopicFilter,
QualityOfServiceLevel = qualityOfServiceLevel,
NoLocal = noLocal,
RetainAsPublished = retainAsPublished,
RetainHandling = retainHandling
});
}
/// <summary>
/// Set the publication topic according to the topic template. The template
/// must not have remaining (unset) parameters or contain wildcards.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>the modified message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
{
if (topicTemplate.Parameters.Any())
{
throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
}
MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
return builder.WithTopic(topicTemplate.Template);
}
}
}
@@ -76,7 +76,7 @@ namespace MQTTnet.Extensions.WebSocket4Net
List<KeyValuePair<string, string>> customHeaders = null; List<KeyValuePair<string, string>> customHeaders = null;
if (_webSocketOptions.RequestHeaders != null) if (_webSocketOptions.RequestHeaders != null)
{ {
customHeaders = _webSocketOptions.RequestHeaders.Select(i => new KeyValuePair<string, string>(i.Key, i.Value)).ToList(); customHeaders = _webSocketOptions.RequestHeaders.ToList();
} }
EndPoint proxy = null; EndPoint proxy = null;
+2 -1
View File
@@ -4,6 +4,7 @@
using MQTTnet.Client; using MQTTnet.Client;
using System; using System;
using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
@@ -27,7 +28,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
} }
}; };
@@ -10,6 +10,7 @@
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
<NoWarn>1591;NETSDK1138</NoWarn> <NoWarn>1591;NETSDK1138</NoWarn>
<LangVersion>7.3</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -20,6 +21,7 @@
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+2 -1
View File
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.IO; using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Internal; using MQTTnet.Internal;
@@ -28,7 +29,7 @@ namespace MQTTnet.TestApp
Credentials = new RandomPassword(), Credentials = new RandomPassword(),
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "broker.hivemq.com" RemoteEndpoint = new DnsEndPoint("broker.hivemq.com", 0)
} }
}, },
+10 -7
View File
@@ -5,13 +5,13 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server; using MQTTnet.Server;
using MqttClient = MQTTnet.Client.MqttClient;
namespace MQTTnet.TestApp namespace MQTTnet.TestApp
{ {
@@ -25,7 +25,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
}, },
CleanSession = true CleanSession = true
}; };
@@ -71,7 +71,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
} }
}; };
@@ -114,7 +114,10 @@ namespace MQTTnet.TestApp
{ {
var options = new MqttClientOptions var options = new MqttClientOptions
{ {
ChannelOptions = new MqttClientTcpOptions { Server = "localhost" }, ChannelOptions = new MqttClientTcpOptions
{
RemoteEndpoint = new DnsEndPoint("localhost", 0)
},
ClientId = "Client1", ClientId = "Client1",
CleanSession = true CleanSession = true
}; };
@@ -218,7 +221,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
}, },
CleanSession = true CleanSession = true
}; };
@@ -269,7 +272,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
}, },
CleanSession = true CleanSession = true
}; };
@@ -320,7 +323,7 @@ namespace MQTTnet.TestApp
{ {
ChannelOptions = new MqttClientTcpOptions ChannelOptions = new MqttClientTcpOptions
{ {
Server = "127.0.0.1" RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0)
}, },
CleanSession = true CleanSession = true
}; };
+32 -23
View File
@@ -42,45 +42,45 @@ namespace MQTTnet.TestApp
}; };
// mqtt.eclipseprojects.io // mqtt.eclipseprojects.io
await ExecuteTestAsync( await ExecuteTestsAsync(
"mqtt.eclipseprojects.io TCP", "mqtt.eclipseprojects.io TCP",
new MqttClientOptionsBuilder().WithTcpServer("mqtt.eclipseprojects.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithTcpServer("mqtt.eclipseprojects.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"mqtt.eclipseprojects.io WS", "mqtt.eclipseprojects.io WS",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:80/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:80/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build());
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
await ExecuteTestAsync("mqtt.eclipseprojects.io WS TLS13", await ExecuteTestsAsync("mqtt.eclipseprojects.io WS TLS13",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt")) new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt"))
.WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build());
await ExecuteTestAsync("mqtt.eclipseprojects.io WS TLS13 (WebSocket4Net)", await ExecuteTestsAsync("mqtt.eclipseprojects.io WS TLS13 (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt")) new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt"))
.WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build(), .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build(),
true); true);
#endif #endif
// test.mosquitto.org // test.mosquitto.org
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org TCP", "test.mosquitto.org TCP",
new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org TCP - Authenticated", "test.mosquitto.org TCP - Authenticated",
new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1884).WithCredentials("rw", "readwrite").WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1884).WithCredentials("rw", "readwrite").WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org TCP TLS12", "test.mosquitto.org TCP TLS12",
new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build());
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
await ExecuteTestAsync("test.mosquitto.org TCP TLS13", await ExecuteTestsAsync("test.mosquitto.org TCP TLS13",
new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883)
.WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build());
#endif #endif
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org TCP TLS12 - Authenticated", "test.mosquitto.org TCP TLS12 - Authenticated",
new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8885) new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8885)
.WithCredentials("rw", "readwrite") .WithCredentials("rw", "readwrite")
@@ -88,67 +88,67 @@ namespace MQTTnet.TestApp
.WithTlsOptions(unsafeTls12) .WithTlsOptions(unsafeTls12)
.Build()); .Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org WS", "test.mosquitto.org WS",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org WS (WebSocket4Net)", "test.mosquitto.org WS (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(), new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(),
true); true);
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org WS TLS12", "test.mosquitto.org WS TLS12",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"test.mosquitto.org WS TLS12 (WebSocket4Net)", "test.mosquitto.org WS TLS12 (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build(), new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build(),
true); true);
// broker.emqx.io // broker.emqx.io
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io TCP", "broker.emqx.io TCP",
new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io TCP TLS12", "broker.emqx.io TCP TLS12",
new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build());
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
await ExecuteTestAsync("broker.emqx.io TCP TLS13", await ExecuteTestsAsync("broker.emqx.io TCP TLS13",
new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883) new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883)
.WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build());
#endif #endif
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io WS", "broker.emqx.io WS",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8083/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8083/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io WS (WebSocket4Net)", "broker.emqx.io WS (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(), new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(),
true); true);
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io WS TLS12", "broker.emqx.io WS TLS12",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.emqx.io WS TLS12 (WebSocket4Net)", "broker.emqx.io WS TLS12 (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build(), new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build(),
true); true);
// broker.hivemq.com // broker.hivemq.com
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.hivemq.com TCP", "broker.hivemq.com TCP",
new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.hivemq.com WS", "broker.hivemq.com WS",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await ExecuteTestAsync( await ExecuteTestsAsync(
"broker.hivemq.com WS (WebSocket4Net)", "broker.hivemq.com WS (WebSocket4Net)",
new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(), new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build(),
true); true);
@@ -160,6 +160,15 @@ namespace MQTTnet.TestApp
Console.ReadLine(); Console.ReadLine();
} }
static async Task ExecuteTestsAsync(string name, MqttClientOptions options, bool useWebSocket4Net = false)
{
options.ProtocolVersion = MqttProtocolVersion.V311;
await ExecuteTestAsync(name + " V3.1.1", options, useWebSocket4Net);
options.ProtocolVersion = MqttProtocolVersion.V500;
await ExecuteTestAsync(name + " V5.0.0", options, useWebSocket4Net);
}
static async Task ExecuteTestAsync(string name, MqttClientOptions options, bool useWebSocket4Net = false) static async Task ExecuteTestAsync(string name, MqttClientOptions options, bool useWebSocket4Net = false)
{ {
try try
+18 -4
View File
@@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using MQTTnet.Extensions.TopicTemplate;
namespace MQTTnet.TestApp namespace MQTTnet.TestApp
{ {
@@ -17,6 +17,8 @@ namespace MQTTnet.TestApp
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>(); singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>(); multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
// Find some reasonable distribution across three topic levels // Find some reasonable distribution across three topic levels
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0));
@@ -40,25 +42,37 @@ namespace MQTTnet.TestApp
int publisherTopicCount = 0; int publisherTopicCount = 0;
var publisherName = "pub" + p; var publisherName = "pub" + p;
var publisherTemplate = baseTemplate
.WithParameter("publisher", publisherName);
for (var l1 = 0; l1 < numLevel1Topics; ++l1) for (var l1 = 0; l1 < numLevel1Topics; ++l1)
{ {
var l1Template = publisherTemplate
.WithParameter("building", "building" + (l1 + 1));
for (var l2 = 0; l2 < numLevel2Topics; ++l2) for (var l2 = 0; l2 < numLevel2Topics; ++l2)
{ {
var l2Template = l1Template
.WithParameter("level", "level" + (l2 + 1));
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
{ {
if (publisherTopicCount >= numTopicsPerPublisher) if (publisherTopicCount >= numTopicsPerPublisher)
break; break;
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1); var topic = l2Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
AddPublisherTopic(publisherName, topic, topicsByPublisher); AddPublisherTopic(publisherName, topic, topicsByPublisher);
if (l2 == 0) if (l2 == 0)
{ {
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1); var singleWildcardTopic = l1Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
if (l1 == 0) if (l1 == 0)
{ {
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l3 + 1); var multiWildcardTopic = publisherTemplate
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
} }
} }
@@ -13,6 +13,7 @@ using MQTTnet.Exceptions;
using MQTTnet.LowLevelClient; using MQTTnet.LowLevelClient;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Tests.Clients.LowLevelMqttClient namespace MQTTnet.Tests.Clients.LowLevelMqttClient
{ {
@@ -13,6 +13,7 @@ using MQTTnet.Diagnostics;
using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server; using MQTTnet.Server;
using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Mockups;
@@ -22,99 +23,6 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
[TestClass] [TestClass]
public sealed class ManagedMqttClient_Tests : BaseTestClass public sealed class ManagedMqttClient_Tests : BaseTestClass
{ {
[TestMethod]
public async Task Expose_Custom_Connection_Error()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
server.ValidatingConnectionAsync += args =>
{
args.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
return CompletedTask.Instance;
};
var managedClient = testEnvironment.Factory.CreateManagedMqttClient();
MqttClientDisconnectedEventArgs disconnectedArgs = null;
managedClient.DisconnectedAsync += args =>
{
disconnectedArgs = args;
return CompletedTask.Instance;
};
var clientOptions = testEnvironment.Factory.CreateManagedMqttClientOptionsBuilder().WithClientOptions(testEnvironment.CreateDefaultClientOptions()).Build();
await managedClient.StartAsync(clientOptions);
await LongTestDelay();
Assert.IsNotNull(disconnectedArgs);
Assert.AreEqual(MqttClientConnectResultCode.BadUserNameOrPassword, disconnectedArgs.ConnectResult.ResultCode);
}
}
[TestMethod]
public async Task Receive_While_Not_Cleanly_Disconnected()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer(o => o.WithPersistentSessions());
var senderClient = await testEnvironment.ConnectClient();
// Prepare managed client.
var managedClient = testEnvironment.Factory.CreateManagedMqttClient();
await managedClient.SubscribeAsync("#");
var receivedMessages = testEnvironment.CreateApplicationMessageHandler(managedClient);
var managedClientOptions = new ManagedMqttClientOptions
{
ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder()
.WithTcpServer("127.0.0.1", testEnvironment.ServerPort)
.WithClientId(nameof(Receive_While_Not_Cleanly_Disconnected) + "_managed")
.WithCleanSession(false)
.Build()
};
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
await LongTestDelay();
// Send test data.
await senderClient.PublishStringAsync("topic1");
await LongTestDelay();
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(1);
// Stop the managed client but keep session at server (not clean disconnect required).
await managedClient.StopAsync(false);
await LongTestDelay();
// Send new messages in the meantime.
await senderClient.PublishStringAsync("topic2", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce);
await LongTestDelay();
// Start the managed client, it should receive the new message.
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(2);
// Stop and start again, no new message should be received.
for (var i = 0; i < 3; i++)
{
await managedClient.StopAsync(false);
await LongTestDelay();
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
}
receivedMessages.AssertReceivedCountEquals(2);
}
}
[TestMethod] [TestMethod]
public async Task Connect_To_Invalid_Server() public async Task Connect_To_Invalid_Server()
{ {
@@ -181,6 +89,38 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
} }
} }
[TestMethod]
public async Task Expose_Custom_Connection_Error()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
server.ValidatingConnectionAsync += args =>
{
args.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
return CompletedTask.Instance;
};
var managedClient = testEnvironment.Factory.CreateManagedMqttClient();
MqttClientDisconnectedEventArgs disconnectedArgs = null;
managedClient.DisconnectedAsync += args =>
{
disconnectedArgs = args;
return CompletedTask.Instance;
};
var clientOptions = testEnvironment.Factory.CreateManagedMqttClientOptionsBuilder().WithClientOptions(testEnvironment.CreateDefaultClientOptions()).Build();
await managedClient.StartAsync(clientOptions);
await LongTestDelay();
Assert.IsNotNull(disconnectedArgs);
Assert.AreEqual(MqttClientConnectResultCode.BadUserNameOrPassword, disconnectedArgs.ConnectResult.ResultCode);
}
}
[TestMethod] [TestMethod]
public async Task ManagedClients_Will_Message_Send() public async Task ManagedClients_Will_Message_Send()
{ {
@@ -224,6 +164,67 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
} }
} }
[TestMethod]
public async Task Receive_While_Not_Cleanly_Disconnected()
{
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
await testEnvironment.StartServer(o => o.WithPersistentSessions());
var senderClient = await testEnvironment.ConnectClient();
// Prepare managed client.
var managedClient = testEnvironment.Factory.CreateManagedMqttClient();
await managedClient.SubscribeAsync("#");
var receivedMessages = testEnvironment.CreateApplicationMessageHandler(managedClient);
var managedClientOptions = new ManagedMqttClientOptions
{
ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder()
.WithTcpServer("127.0.0.1", testEnvironment.ServerPort)
.WithClientId(nameof(Receive_While_Not_Cleanly_Disconnected) + "_managed")
.WithCleanSession(false)
.Build()
};
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
await LongTestDelay();
// Send test data.
await senderClient.PublishStringAsync("topic1");
await LongTestDelay();
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(1);
// Stop the managed client but keep session at server (not clean disconnect required).
await managedClient.StopAsync(false);
await LongTestDelay();
// Send new messages in the meantime.
await senderClient.PublishStringAsync("topic2", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce);
await LongTestDelay();
// Start the managed client, it should receive the new message.
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
receivedMessages.AssertReceivedCountEquals(2);
// Stop and start again, no new message should be received.
for (var i = 0; i < 3; i++)
{
await managedClient.StopAsync(false);
await LongTestDelay();
await managedClient.StartAsync(managedClientOptions);
await LongTestDelay();
}
receivedMessages.AssertReceivedCountEquals(2);
}
}
[TestMethod] [TestMethod]
public async Task Start_Stop() public async Task Start_Stop()
{ {
@@ -375,7 +376,7 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort); var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort);
var receivedManagedMessages = new List<MqttApplicationMessage>(); var receivedManagedMessages = new List<MqttApplicationMessage>();
var managedClient = testEnvironment.Factory.CreateManagedMqttClient(testEnvironment.CreateClient()); var managedClient = testEnvironment.Factory.CreateManagedMqttClient(testEnvironment.CreateClient());
managedClient.ApplicationMessageReceivedAsync += e => managedClient.ApplicationMessageReceivedAsync += e =>
{ {
@@ -403,7 +404,7 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
// Make sure that it gets received after subscribing again. // Make sure that it gets received after subscribing again.
await managedClient.SubscribeAsync("topic"); await managedClient.SubscribeAsync("topic");
await LongTestDelay(); await LongTestDelay();
Assert.AreEqual(2, receivedManagedMessages.Count); Assert.AreEqual(2, receivedManagedMessages.Count);
} }
} }
@@ -421,7 +422,7 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
var receivingClient = await CreateManagedClientAsync(testEnvironment, null, connectionCheckInterval); var receivingClient = await CreateManagedClientAsync(testEnvironment, null, connectionCheckInterval);
var sendingClient = await testEnvironment.ConnectClient(); var sendingClient = await testEnvironment.ConnectClient();
await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", PayloadSegment = new ArraySegment<byte>( new byte[] { 1 }), Retain = true }); await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", PayloadSegment = new ArraySegment<byte>(new byte[] { 1 }), Retain = true });
var subscribeTime = DateTime.UtcNow; var subscribeTime = DateTime.UtcNow;
@@ -437,6 +438,109 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
} }
} }
[TestMethod]
public async Task ManagedClients_CanInterceptPublishedMessage_PreventingPublish()
{
using (var testEnvironment = CreateTestEnvironment())
{
await testEnvironment.StartServer();
var receivedMessagesCount = 0;
var interceptedMessagesCount = 0;
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", testEnvironment.ServerPort)
.Build();
var sendingClient = testEnvironment.CreateClient();
var sendingManagedClient = new MQTTnet.Extensions.ManagedClient.ManagedMqttClient(sendingClient, testEnvironment.ClientLogger);
await sendingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build());
// Wait until the managed client is fully set up and running.
await Task.Delay(1000);
var receivingClient = await testEnvironment.ConnectClient();
receivingClient.ApplicationMessageReceivedAsync += e =>
{
Interlocked.Increment(ref receivedMessagesCount);
return CompletedTask.Instance;
};
sendingManagedClient.InterceptPublishMessageAsync += e =>
{
Interlocked.Increment(ref interceptedMessagesCount);
e.AcceptPublish = false;
return CompletedTask.Instance;
};
await receivingClient.SubscribeAsync("My/last/will");
// Wait for arrival of the will message at the receiver.
await Task.Delay(5000);
await sendingManagedClient.EnqueueAsync("My/last/will", "hello world");
// Wait for arrival of the will message at the receiver.
await Task.Delay(5000);
Assert.AreEqual(0, receivedMessagesCount);
Assert.AreEqual(1, interceptedMessagesCount);
}
}
[TestMethod]
public async Task ManagedClients_CanInterceptPublishedMessage_AllowingPublish()
{
using (var testEnvironment = CreateTestEnvironment())
{
await testEnvironment.StartServer();
var receivedMessagesCount = 0;
var interceptedMessagesCount = 0;
var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", testEnvironment.ServerPort)
.Build();
var sendingClient = testEnvironment.CreateClient();
var sendingManagedClient = new MQTTnet.Extensions.ManagedClient.ManagedMqttClient(sendingClient, testEnvironment.ClientLogger);
await sendingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build());
// Wait until the managed client is fully set up and running.
await Task.Delay(1000);
var receivingClient = await testEnvironment.ConnectClient();
receivingClient.ApplicationMessageReceivedAsync += e =>
{
Interlocked.Increment(ref receivedMessagesCount);
return CompletedTask.Instance;
};
sendingManagedClient.InterceptPublishMessageAsync += e =>
{
Interlocked.Increment(ref interceptedMessagesCount);
e.AcceptPublish = true;
return CompletedTask.Instance;
};
await receivingClient.SubscribeAsync("My/last/will");
// Wait for arrival of the will message at the receiver.
await Task.Delay(5000);
await sendingManagedClient.EnqueueAsync("My/last/will", "hello world");
// Wait for arrival of the will message at the receiver.
await Task.Delay(5000);
Assert.AreEqual(1, receivedMessagesCount);
Assert.AreEqual(1, interceptedMessagesCount);
}
}
// This case also serves as a regression test for the previous behavior which re-published // This case also serves as a regression test for the previous behavior which re-published
// each and every existing subscriptions with every new subscription that was made // each and every existing subscriptions with every new subscription that was made
// (causing performance problems and having the visible symptom of retained messages being received again) // (causing performance problems and having the visible symptom of retained messages being received again)
@@ -471,16 +575,154 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
} }
} }
[TestMethod]
public async Task Subscribe_Does_Not_Hang_On_Server_Stop()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_2";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new SemaphoreSlim(0, 1);
var failedOnClient = new SemaphoreSlim(0, 1);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttSubscribePacket)
{
if (reject)
{
e.ProcessPacket = false;
}
receivedOnServer.Release();
}
return CompletedTask.Instance;
};
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
{
failedOnClient.Release();
return CompletedTask.Instance;
};
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
reject = false;
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
}
}
[TestMethod]
public async Task Unsubscribe_Does_Not_Hang_On_Server_Stop()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_2";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new SemaphoreSlim(0, 1);
var failedOnClient = new SemaphoreSlim(0, 1);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttUnsubscribePacket)
{
if (reject)
{
e.ProcessPacket = false;
}
receivedOnServer.Release();
}
else if (e.Packet is MqttSubscribePacket)
{
receivedOnServer.Release();
}
return CompletedTask.Instance;
};
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
{
failedOnClient.Release();
return CompletedTask.Instance;
};
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
await managedClient.UnsubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
reject = false;
await managedClient.UnsubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
}
}
[TestMethod]
public async Task Publish_Does_Not_Hang_On_Server_Error()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_42";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new TaskCompletionSource<object>();
managedClient.ApplicationMessageProcessedAsync += e => Task.FromResult(reject &= e.Exception is null);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttPublishPacket)
{
if (reject)
{
e.ProcessPacket = false;
}
else
{
receivedOnServer.TrySetResult(null);
}
}
return CompletedTask.Instance;
};
await managedClient.EnqueueAsync(new MqttApplicationMessage { Topic = topic, PayloadSegment = new ArraySegment<byte>(new byte[] { 1 }), Retain = true, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce });
var timeoutTask = Task.Delay(testTimeout);
var firstDone = await Task.WhenAny(receivedOnServer.Task, timeoutTask);
Assert.AreEqual(receivedOnServer.Task, firstDone, "Client is hung on publish!");
}
}
async Task<MQTTnet.Extensions.ManagedClient.ManagedMqttClient> CreateManagedClientAsync( async Task<MQTTnet.Extensions.ManagedClient.ManagedMqttClient> CreateManagedClientAsync(
TestEnvironment testEnvironment, TestEnvironment testEnvironment,
IMqttClient underlyingClient = null, IMqttClient underlyingClient = null,
TimeSpan? connectionCheckInterval = null, TimeSpan? connectionCheckInterval = null,
string host = "localhost") string host = "localhost",
TimeSpan? timeout = null)
{ {
await testEnvironment.StartServer(); await testEnvironment.StartServer();
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort); var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort);
if (timeout != null)
{
clientOptions.WithTimeout(timeout.Value);
}
var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build(); var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build();
// Use a short connection check interval so that subscription operations are performed quickly // Use a short connection check interval so that subscription operations are performed quickly
@@ -65,7 +65,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build()); await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build());
Assert.Fail("Must fail!"); Assert.Fail("Must fail!");
} }
@@ -167,6 +167,38 @@ namespace MQTTnet.Tests.Clients.MqttClient
} }
} }
[TestMethod]
public async Task No_Unobserved_Exception()
{
using (var testEnvironment = CreateTestEnvironment())
{
testEnvironment.IgnoreClientLogErrors = true;
var client = testEnvironment.CreateClient();
var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build();
try
{
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5)))
{
await client.ConnectAsync(options, timeout.Token);
}
}
catch (OperationCanceledException)
{
}
client.Dispose();
// These delays and GC calls are required in order to make calling the finalizer reproducible.
GC.Collect();
GC.WaitForPendingFinalizers();
await LongTestDelay();
await LongTestDelay();
await LongTestDelay();
}
}
[TestMethod] [TestMethod]
public async Task Return_Non_Success() public async Task Return_Non_Success()
{ {
@@ -208,9 +240,9 @@ namespace MQTTnet.Tests.Clients.MqttClient
await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce);
} }
} }
catch (MqttCommunicationException exception) catch (MqttClientNotConnectedException exception)
{ {
if (exception.Message == "The client is not connected.") if (exception.Message == "The MQTT client is not connected.")
{ {
return; return;
} }
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -17,6 +18,7 @@ using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server;
using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Mockups;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@@ -85,7 +87,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
await client.ConnectAsync(clientOptions); await client.ConnectAsync(clientOptions);
} }
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(InvalidOperationException))] [ExpectedException(typeof(InvalidOperationException))]
public async Task Connect_Multiple_Times_Should_Fail() public async Task Connect_Multiple_Times_Should_Fail()
@@ -747,8 +749,8 @@ namespace MQTTnet.Tests.Clients.MqttClient
{ {
if (e.ApplicationMessage.Topic == "request") if (e.ApplicationMessage.Topic == "request")
{ {
// Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for // Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for
// the ACK etc. The problem is that the SpinUntil below only waits until the // the ACK etc. The problem is that the SpinUntil below only waits until the
// flag is set. It does not wait until the client has sent the ACK // flag is set. It does not wait until the client has sent the ACK
await client2.PublishStringAsync("reply"); await client2.PublishStringAsync("reply");
} }
@@ -924,5 +926,54 @@ namespace MQTTnet.Tests.Clients.MqttClient
Assert.IsFalse(disconnectedFired); Assert.IsFalse(disconnectedFired);
} }
} }
[TestMethod]
public void Backward_compatible_TCP_options()
{
var options = new MqttClientOptionsBuilder().WithTcpServer("host", 3).Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptions
{
ChannelOptions = new MqttClientTcpOptions
{
Server = "host",
Port = 3
}
};
Assert.AreEqual("host:3", options.ChannelOptions.ToString());
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptionsBuilder().WithEndPoint(new DnsEndPoint("host", 3)).Build();
Assert.AreEqual("Unspecified/host:3", options.ChannelOptions.ToString());
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptionsBuilder().WithTcpServer("host").Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(1883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
Assert.AreEqual("Unspecified/host:1883", options.ChannelOptions.ToString());
options = new MqttClientOptionsBuilder().WithTlsOptions(o => o.UseTls()).WithTcpServer("host").Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(8883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptions
{
ChannelOptions = new MqttClientTcpOptions
{
Server = "host"
}
};
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(null, ((MqttClientTcpOptions)options.ChannelOptions).Port);
}
} }
} }
@@ -0,0 +1,273 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Exceptions;
using MQTTnet.Extensions.TopicTemplate;
namespace MQTTnet.Tests.Extensions
{
[TestClass]
public sealed class MqttTopicTemplate_Tests
{
[TestMethod]
public void BasicOps()
{
var template = new MqttTopicTemplate("A/B/{param}/D");
Assert.AreEqual("A/B/C/D", template.WithParameter("param", "C").Template);
Assert.AreEqual("A/B/+/D", template.WithoutParameter("param").Template);
Assert.AreEqual("A/B/+/D", template.TopicFilter);
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
// corner cases
Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter);
Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter);
Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter);
}
[TestMethod]
public void BasicOpsWithHash()
{
var template = new MqttTopicTemplate("A/B/{param}/D/#");
Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "C").Template);
Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param").Template);
Assert.AreEqual("A/B/+/D/#", template.TopicFilter);
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars1()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a#");
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars2()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a+b");
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars3()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a/b");
}
[TestMethod]
public void AcceptsEmptyValue()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "");
}
[TestMethod]
[ExpectedException(typeof(MqttProtocolViolationException))]
public void RejectsEmptyTemplate()
{
var _ = new MqttTopicTemplate("");
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void RejectsNullTemplate()
{
var _ = new MqttTopicTemplate(null);
}
[TestMethod]
public void IgnoresEmptyParameters()
{
var template = new MqttTopicTemplate("A/B/{}/D");
Assert.IsFalse(template.Parameters.Any());
}
[TestMethod]
public void AcceptsValidTopics()
{
_ = new MqttTopicTemplate("A");
_ = new MqttTopicTemplate("/");
_ = new MqttTopicTemplate("/A");
_ = new MqttTopicTemplate("A/#");
_ = new MqttTopicTemplate("$SYS/#");
_ = new MqttTopicTemplate("////");
_ = new MqttTopicTemplate("A/B/{foo}/D/");
}
[TestMethod]
public void DynamicRoutingSupport()
{
var v1template = new MqttTopicTemplate("A/v1/{id}/F");
var v2template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}");
var incoming_v1_topic = "A/v1/32/F";
var compat_v2_topic = v2template
.WithParameterValuesFrom(
v1template.ParseParameterValues(incoming_v1_topic))
.WithParameter("bar", "unknown");
// id: 32 was relocated and bar: unknown added
Assert.AreEqual("A/v2/foo/32/unknown", compat_v2_topic.Template);
}
[TestMethod]
public void SubscriptionSupport()
{
var template = new MqttTopicTemplate("A/v1/{param}/F");
var filter = template.BuildFilter()
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
Assert.AreEqual("A/v1/+/F", filter.Topic);
}
[TestMethod]
public void SubscriptionSupport2()
{
var template = new MqttTopicTemplate("A/v1/{param}/F");
var subscribeOptions = new MqttFactory().CreateSubscribeOptionsBuilder()
.WithTopicTemplate(template)
.WithSubscriptionIdentifier(5)
.Build();
Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic);
}
[TestMethod]
public void SendAndSubscribeSupport()
{
var template = new MqttTopicTemplate("App/v1/{sender}/message");
var filter = new MqttTopicFilterBuilder()
.WithTopicTemplate(template)
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
Assert.AreEqual("App/v1/+/message", filter.Topic);
var myTopic = template.WithParameter("sender", "me, myself & i");
var message = new MqttApplicationMessageBuilder()
.WithTopicTemplate(
myTopic)
.WithPayload("Hello!")
.Build();
Assert.IsTrue(message.MatchesTopicTemplate(template));
}
[TestMethod]
public void SendAndSubscribeSupport2()
{
var template = new MqttTopicTemplate("App/v1/{sender}/message");
Assert.ThrowsException<ArgumentException>(() =>
template.BuildMessage());
}
[TestMethod]
public void CanonicalPrefixFilter()
{
var template1 = new MqttTopicTemplate("A/v1/{param}/F");
var template2 = new MqttTopicTemplate("A/v1/E/F");
var template3 = new MqttTopicTemplate("A/v1/E/F/G/H");
var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3 });
// possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
var template2b = new MqttTopicTemplate("A/v1/E/X");
canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template2b });
Assert.AreEqual("A/v1/+", canonicalFilter.Template);
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
var template4 = new MqttTopicTemplate("A/v2/{prefix}");
var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template4 });
Assert.AreEqual("A/+", canonicalFilter2.TopicFilter);
Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple1()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A"),
new MqttTopicTemplate("B")
});
Assert.AreEqual("#", template.TopicFilter);
Assert.AreEqual("#", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple2()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/F/"),
new MqttTopicTemplate("A/v1/E/X")
});
Assert.AreEqual("A/v1/+", template.TopicFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple3()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/F"),
new MqttTopicTemplate("A/v1/{param}/F/X")
});
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple4()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/FFA"),
new MqttTopicTemplate("A/v1/{param}/FX")
});
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple5()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/E/F/G/H"),
new MqttTopicTemplate("A/v2/{param}")
});
Assert.AreEqual("A/+", template.TopicFilter);
Assert.AreEqual("A/+/#", template.TopicTreeRootFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple6()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/{version}/E/F/G/H"),
new MqttTopicTemplate("A/{version}/{param}")
});
Assert.AreEqual("A/+/+", template.TopicFilter);
Assert.AreEqual("A/+/+/#", template.TopicTreeRootFilter);
}
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ namespace MQTTnet.Tests.Extensions
var paramValue = "123"; var paramValue = "123";
var parameters = new Dictionary<string, object> var parameters = new Dictionary<string, object>
{ {
{ TestParametersTopicGenerationStrategy.ExpectedParamName, "123" }, { TestParametersTopicGenerationStrategy.ExpectedParamName, "123" }
}; };
using (var testEnvironment = CreateTestEnvironment()) using (var testEnvironment = CreateTestEnvironment())
@@ -164,7 +164,7 @@ namespace MQTTnet.Tests.Extensions
using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build())) using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()))
{ {
var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce);
} }
} }
} }
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -17,7 +18,7 @@ namespace MQTTnet.Tests.Internal
[TestMethod] [TestMethod]
public async Task Connect_Send_Receive() public async Task Connect_Send_Receive()
{ {
var crossPlatformSocket = new CrossPlatformSocket(); var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
await crossPlatformSocket.ConnectAsync("www.google.de", 80, CancellationToken.None); await crossPlatformSocket.ConnectAsync("www.google.de", 80, CancellationToken.None);
var requestBuffer = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nHost: www.google.de\r\n\r\n"); var requestBuffer = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nHost: www.google.de\r\n\r\n");
@@ -36,7 +37,7 @@ namespace MQTTnet.Tests.Internal
[ExpectedException(typeof(OperationCanceledException))] [ExpectedException(typeof(OperationCanceledException))]
public async Task Try_Connect_Invalid_Host() public async Task Try_Connect_Invalid_Host()
{ {
var crossPlatformSocket = new CrossPlatformSocket(); var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5));
cancellationToken.Token.Register(() => crossPlatformSocket.Dispose()); cancellationToken.Token.Register(() => crossPlatformSocket.Dispose());
@@ -65,7 +66,7 @@ namespace MQTTnet.Tests.Internal
[TestMethod] [TestMethod]
public void Set_Options() public void Set_Options()
{ {
var crossPlatformSocket = new CrossPlatformSocket(); var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
Assert.IsFalse(crossPlatformSocket.ReuseAddress); Assert.IsFalse(crossPlatformSocket.ReuseAddress);
crossPlatformSocket.ReuseAddress = true; crossPlatformSocket.ReuseAddress = true;
+3 -1
View File
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;net461;net48;netcoreapp3.1;net6.0;net7.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net48</TargetFrameworks>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<LangVersion>7.3</LangVersion> <LangVersion>7.3</LangVersion>
<EnableNETAnalyzers>false</EnableNETAnalyzers> <EnableNETAnalyzers>false</EnableNETAnalyzers>
@@ -20,6 +21,7 @@
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" /> <ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" /> <ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
</ItemGroup> </ItemGroup>
+1 -1
View File
@@ -273,7 +273,7 @@ namespace MQTTnet.Tests.MQTTv5
Assert.AreEqual(applicationMessage.ResponseTopic, receivedMessage.ResponseTopic); Assert.AreEqual(applicationMessage.ResponseTopic, receivedMessage.ResponseTopic);
Assert.AreEqual(applicationMessage.MessageExpiryInterval, receivedMessage.MessageExpiryInterval); Assert.AreEqual(applicationMessage.MessageExpiryInterval, receivedMessage.MessageExpiryInterval);
CollectionAssert.AreEqual(applicationMessage.CorrelationData, receivedMessage.CorrelationData); CollectionAssert.AreEqual(applicationMessage.CorrelationData, receivedMessage.CorrelationData);
CollectionAssert.AreEqual(applicationMessage.Payload, receivedMessage.Payload); CollectionAssert.AreEqual(applicationMessage.PayloadSegment.ToArray(), receivedMessage.PayloadSegment.ToArray());
CollectionAssert.AreEqual(applicationMessage.UserProperties, receivedMessage.UserProperties); CollectionAssert.AreEqual(applicationMessage.UserProperties, receivedMessage.UserProperties);
} }
} }
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Tests.MQTTv5 namespace MQTTnet.Tests.MQTTv5
{ {
+87 -39
View File
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -25,9 +24,9 @@ namespace MQTTnet.Tests.Mockups
public sealed class TestEnvironment : IDisposable public sealed class TestEnvironment : IDisposable
{ {
readonly List<string> _clientErrors = new List<string>(); readonly List<string> _clientErrors = new List<string>();
readonly ConcurrentBag<IMqttClient> _clients = new ConcurrentBag<IMqttClient>(); readonly List<IMqttClient> _clients = new List<IMqttClient>();
readonly List<Exception> _exceptions = new List<Exception>(); readonly List<Exception> _exceptions = new List<Exception>();
readonly ConcurrentBag<ILowLevelMqttClient> _lowLevelClients = new ConcurrentBag<ILowLevelMqttClient>(); readonly List<ILowLevelMqttClient> _lowLevelClients = new List<ILowLevelMqttClient>();
readonly MqttProtocolVersion _protocolVersion; readonly MqttProtocolVersion _protocolVersion;
readonly List<string> _serverErrors = new List<string>(); readonly List<string> _serverErrors = new List<string>();
@@ -40,6 +39,8 @@ namespace MQTTnet.Tests.Mockups
_protocolVersion = protocolVersion; _protocolVersion = protocolVersion;
TestContext = testContext; TestContext = testContext;
TaskScheduler.UnobservedTaskException += TrackUnobservedTaskException;
ServerLogger.LogMessagePublished += (s, e) => ServerLogger.LogMessagePublished += (s, e) =>
{ {
if (Debugger.IsAttached) if (Debugger.IsAttached)
@@ -216,8 +217,9 @@ namespace MQTTnet.Tests.Mockups
public IMqttClient CreateClient() public IMqttClient CreateClient()
{ {
var logger = EnableLogger ? (IMqttNetLogger)ClientLogger : MqttNetNullLogger.Instance; var logger = EnableLogger ? (IMqttNetLogger)ClientLogger : MqttNetNullLogger.Instance;
var client = Factory.CreateMqttClient(logger); var client = Factory.CreateMqttClient(logger);
client.ConnectingAsync += e => client.ConnectingAsync += e =>
{ {
if (TestContext != null) if (TestContext != null)
@@ -232,9 +234,12 @@ namespace MQTTnet.Tests.Mockups
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
_clients.Add(client); lock (_clients)
{
_clients.Add(client);
}
return client; return client;
} }
@@ -245,13 +250,20 @@ namespace MQTTnet.Tests.Mockups
public MqttClientOptionsBuilder CreateDefaultClientOptionsBuilder() public MqttClientOptionsBuilder CreateDefaultClientOptionsBuilder()
{ {
return Factory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion).WithTcpServer("127.0.0.1", ServerPort).WithClientId(TestContext.TestName + "_" + Guid.NewGuid()); return Factory.CreateClientOptionsBuilder()
.WithProtocolVersion(_protocolVersion)
.WithTcpServer("127.0.0.1", ServerPort)
.WithClientId(TestContext.TestName + "_" + Guid.NewGuid());
} }
public ILowLevelMqttClient CreateLowLevelClient() public ILowLevelMqttClient CreateLowLevelClient()
{ {
var client = Factory.CreateLowLevelMqttClient(ClientLogger); var client = Factory.CreateLowLevelMqttClient(ClientLogger);
_lowLevelClients.Add(client);
lock (_lowLevelClients)
{
_lowLevelClients.Add(client);
}
return client; return client;
} }
@@ -287,45 +299,68 @@ namespace MQTTnet.Tests.Mockups
public void Dispose() public void Dispose()
{ {
foreach (var mqttClient in _clients) try
{ {
lock (_clients)
{
foreach (var mqttClient in _clients)
{
try
{
//mqttClient.DisconnectAsync().GetAwaiter().GetResult();
}
catch
{
// This can happen when the test already disconnected the client.
}
finally
{
mqttClient?.Dispose();
}
}
_clients.Clear();
}
lock (_lowLevelClients)
{
foreach (var lowLevelMqttClient in _lowLevelClients)
{
lowLevelMqttClient.Dispose();
}
_lowLevelClients.Clear();
}
try try
{ {
//mqttClient.DisconnectAsync().GetAwaiter().GetResult(); Server?.StopAsync().GetAwaiter().GetResult();
} }
catch catch
{ {
// This can happen when the test already disconnected the client. // This can happen when the test already stopped the server.
} }
finally finally
{ {
mqttClient?.Dispose(); Server?.Dispose();
} }
}
foreach (var lowLevelMqttClient in _lowLevelClients) Server = null;
{
lowLevelMqttClient.Dispose();
}
try ThrowIfLogErrors();
{
Server?.StopAsync().GetAwaiter().GetResult(); GC.Collect();
} GC.WaitForFullGCComplete();
catch GC.WaitForPendingFinalizers();
{
// This can happen when the test already stopped the server. if (_exceptions.Any())
{
throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions));
}
} }
finally finally
{ {
Server?.Dispose(); TaskScheduler.UnobservedTaskException -= TrackUnobservedTaskException;
}
ThrowIfLogErrors();
if (_exceptions.Any())
{
throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions));
} }
} }
@@ -342,7 +377,7 @@ namespace MQTTnet.Tests.Mockups
var options = optionsBuilder.Build(); var options = optionsBuilder.Build();
var server = CreateServer(options); var server = CreateServer(options);
await server.StartAsync(); await server.StartAsync().ConfigureAwait(false);
// The OS has chosen the port to we have to properly expose it to the tests. // The OS has chosen the port to we have to properly expose it to the tests.
ServerPort = options.DefaultEndpointOptions.Port; ServerPort = options.DefaultEndpointOptions.Port;
@@ -370,13 +405,16 @@ namespace MQTTnet.Tests.Mockups
public void ThrowIfLogErrors() public void ThrowIfLogErrors()
{ {
lock (_serverErrors) if (!IgnoreServerLogErrors)
{ {
if (!IgnoreServerLogErrors && _serverErrors.Count > 0) lock (_serverErrors)
{ {
var message = $"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."; if (_serverErrors.Count > 0)
Console.WriteLine(message); {
throw new Exception(message); var message = $"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)}).";
Console.WriteLine(message);
throw new Exception(message);
}
} }
} }
@@ -396,10 +434,20 @@ namespace MQTTnet.Tests.Mockups
public void TrackException(Exception exception) public void TrackException(Exception exception)
{ {
if (exception == null)
{
return;
}
lock (_exceptions) lock (_exceptions)
{ {
_exceptions.Add(exception); _exceptions.Add(exception);
} }
} }
void TrackUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
TrackException(e.Exception);
}
} }
} }
@@ -1,3 +1,5 @@
#pragma warning disable CS0618 // Type or member is obsolete
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -8,7 +10,7 @@ using System.Threading.Tasks;
namespace MQTTnet.Tests namespace MQTTnet.Tests
{ {
[TestClass] [TestClass]
public class MqttApplicationMessageTest public sealed class MqttApplicationMessageTest
{ {
[TestMethod] [TestMethod]
public void PayloadSegment() public void PayloadSegment()
@@ -17,32 +19,27 @@ namespace MQTTnet.Tests
Assert.AreEqual(0, message.Payload.Length); Assert.AreEqual(0, message.Payload.Length);
Assert.AreEqual(0, message.PayloadSegment.Count); Assert.AreEqual(0, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload)); Assert.IsTrue(ReferenceEquals(message.Payload, message.PayloadSegment.Array));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.Payload = new byte[] { 1, 2 }; message.Payload = new byte[] { 1, 2 };
Assert.AreEqual(2, message.Payload.Length); Assert.AreEqual(2, message.Payload.Length);
Assert.AreEqual(2, message.PayloadSegment.Count); Assert.AreEqual(2, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload)); Assert.IsTrue(ReferenceEquals(message.Payload, message.PayloadSegment.Array));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.Payload = new byte[] { 1, 2, 3 }; message.Payload = new byte[] { 1, 2, 3 };
Assert.AreEqual(3, message.Payload.Length); Assert.AreEqual(3, message.Payload.Length);
Assert.AreEqual(3, message.PayloadSegment.Count); Assert.AreEqual(3, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload)); Assert.IsTrue(ReferenceEquals(message.Payload, message.PayloadSegment.Array));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 }); message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 });
Assert.AreEqual(3, message.Payload.Length); Assert.AreEqual(3, message.Payload.Length);
Assert.AreEqual(3, message.PayloadSegment.Count); Assert.AreEqual(3, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload)); Assert.IsTrue(ReferenceEquals(message.Payload, message.PayloadSegment.Array));
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 }, 1, 1); message.PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3 }, 1, 1);
Assert.AreEqual(1, message.Payload.Length); Assert.AreEqual(1, message.Payload.Length);
Assert.AreEqual(1, message.PayloadSegment.Count); Assert.AreEqual(1, message.PayloadSegment.Count);
Assert.IsTrue(object.ReferenceEquals(message.Payload, message.Payload)); Assert.IsFalse(ReferenceEquals(message.Payload, message.PayloadSegment.Array));
Assert.IsFalse(object.ReferenceEquals(message.Payload, message.PayloadSegment.Array));
} }
} }
} }
+56 -50
View File
@@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Implementations;
using System; using System;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Implementations;
namespace MQTTnet.Tests namespace MQTTnet.Tests
{ {
@@ -18,62 +18,68 @@ namespace MQTTnet.Tests
[TestMethod] [TestMethod]
public async Task Dispose_Channel_While_Used() public async Task Dispose_Channel_While_Used()
{ {
var ct = new CancellationTokenSource(); using (var ct = new CancellationTokenSource())
var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
try
{ {
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001)); using (var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp))
serverSocket.Listen(0);
#pragma warning disable 4014
Task.Run(async () =>
#pragma warning restore 4014
{ {
while (!ct.IsCancellationRequested) try
{ {
var client = await serverSocket.AcceptAsync(); serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
var data = new byte[] { 128 }; serverSocket.Listen(0);
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
}
}, ct.Token);
var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
await Task.Delay(100, ct.Token);
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 #pragma warning disable 4014
Task.Run(() => Task.Run(
async () =>
#pragma warning restore 4014 #pragma warning restore 4014
{ {
Task.Delay(200, ct.Token); while (!ct.IsCancellationRequested)
tcpChannel.Dispose(); {
}, ct.Token); var client = await serverSocket.AcceptAsync(ct.Token);
var data = new byte[] { 128 };
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
}
},
ct.Token);
try var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
{ await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None);
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
await Task.Delay(100, ct.Token);
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);
}
} }
catch (Exception exception)
{
Assert.IsInstanceOfType(exception, typeof(SocketException));
Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode);
}
}
finally
{
ct.Cancel(false);
serverSocket.Dispose();
} }
} }
} }
} }
@@ -23,7 +23,7 @@ namespace MQTTnet.Tests.Server
{ {
await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1)));
var client = new CrossPlatformSocket(AddressFamily.InterNetwork); var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None); await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
// Don't send anything. The server should close the connection. // Don't send anything. The server should close the connection.
@@ -54,7 +54,7 @@ namespace MQTTnet.Tests.Server
// Send an invalid packet and ensure that the server will close the connection and stay in a waiting state // Send an invalid packet and ensure that the server will close the connection and stay in a waiting state
// forever. This is security related. // forever. This is security related.
var client = new CrossPlatformSocket(AddressFamily.InterNetwork); var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None); await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
var buffer = Encoding.UTF8.GetBytes("Garbage"); var buffer = Encoding.UTF8.GetBytes("Garbage");
+4 -4
View File
@@ -303,7 +303,7 @@ namespace MQTTnet.Tests.Server
var server = await testEnvironment.StartServer(); var server = await testEnvironment.StartServer();
server.InterceptingPublishAsync += e => server.InterceptingPublishAsync += e =>
{ {
e.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); e.ApplicationMessage.PayloadSegment = new ArraySegment<byte>(Encoding.ASCII.GetBytes("extended"));
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
@@ -314,7 +314,7 @@ namespace MQTTnet.Tests.Server
var isIntercepted = false; var isIntercepted = false;
c2.ApplicationMessageReceivedAsync += e => c2.ApplicationMessageReceivedAsync += e =>
{ {
isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment.ToArray()), StringComparison.Ordinal) == 0;
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
@@ -425,7 +425,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "/test/1", Topic = "/test/1",
Payload = Encoding.UTF8.GetBytes("true"), PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes("true")),
QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce
}) })
{ {
@@ -780,7 +780,7 @@ namespace MQTTnet.Tests.Server
var client1 = await testEnvironment.ConnectClient(); var client1 = await testEnvironment.ConnectClient();
client1.ApplicationMessageReceivedAsync += e => client1.ApplicationMessageReceivedAsync += e =>
{ {
receivedBody = e.ApplicationMessage.Payload; receivedBody = e.ApplicationMessage.PayloadSegment.ToArray();
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
@@ -1,4 +1,4 @@
#if !(NET452 || NET461) #if !(NET452 || NET461 || NET48)
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
@@ -149,18 +149,14 @@ namespace MQTTnet.Tests.Server
} }
} }
class ClientTestHarness : IDisposable sealed class ClientTestHarness : IDisposable
{ {
readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider(); readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider();
IMqttClient _client; IMqttClient _client;
public string ClientId => _client.Options.ClientId; public string ClientId => _client.Options.ClientId;
public void ClearServerCerts()
{
_hotSwapClient.ClearServerCerts();
}
public Task Connect() public Task Connect()
{ {
return Run_Client_Connection(); return Run_Client_Connection();
@@ -169,6 +165,7 @@ namespace MQTTnet.Tests.Server
public void Dispose() public void Dispose()
{ {
_client.Dispose(); _client.Dispose();
_hotSwapClient.Dispose();
} }
public X509Certificate2 GetCurrentClientCert() public X509Certificate2 GetCurrentClientCert()
@@ -189,6 +186,8 @@ namespace MQTTnet.Tests.Server
public void WaitForConnectOrFail(TimeSpan timeout) public void WaitForConnectOrFail(TimeSpan timeout)
{ {
Thread.Sleep(100);
if (!_client.IsConnected) if (!_client.IsConnected)
{ {
_client.ReconnectAsync().Wait(timeout); _client.ReconnectAsync().Wait(timeout);
@@ -210,12 +209,12 @@ namespace MQTTnet.Tests.Server
Assert.IsFalse(_client.IsConnected, "Client connection success but test wanted fail"); Assert.IsFalse(_client.IsConnected, "Client connection success but test wanted fail");
} }
public void WaitForDisconnect(TimeSpan timeout) void WaitForDisconnect(TimeSpan timeout)
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
while ((_client == null || _client.IsConnected) && timer.Elapsed < timeout) while ((_client == null || _client.IsConnected) && timer.Elapsed < timeout)
{ {
Thread.Sleep(5); Thread.Sleep(100);
} }
} }
@@ -252,22 +251,17 @@ namespace MQTTnet.Tests.Server
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
while ((_client == null || !_client.IsConnected) && timer.Elapsed < timeout) while ((_client == null || !_client.IsConnected) && timer.Elapsed < timeout)
{ {
Thread.Sleep(5); Thread.Sleep(100);
} }
} }
} }
class ServerTestHarness : IDisposable sealed class ServerTestHarness : IDisposable
{ {
readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider(); readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider();
MqttServer _server; MqttServer _server;
public void ClearClientCerts()
{
_hotSwapServer.ClearClientCerts();
}
public void Dispose() public void Dispose()
{ {
if (_server != null) if (_server != null)
@@ -276,15 +270,12 @@ namespace MQTTnet.Tests.Server
_server.Dispose(); _server.Dispose();
} }
if (_hotSwapServer != null) _hotSwapServer?.Dispose();
{
_hotSwapServer.Dispose();
}
} }
public async Task ForceDisconnectAsync(ClientTestHarness client) public Task ForceDisconnectAsync(ClientTestHarness client)
{ {
await _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError); return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError);
} }
public X509Certificate2 GetCurrentServerCert() public X509Certificate2 GetCurrentServerCert()
@@ -302,7 +293,7 @@ namespace MQTTnet.Tests.Server
_hotSwapServer.InstallNewClientCert(serverCert); _hotSwapServer.InstallNewClientCert(serverCert);
} }
public async Task StartServer() public Task StartServer()
{ {
var mqttFactory = new MqttFactory(); var mqttFactory = new MqttFactory();
@@ -312,24 +303,33 @@ namespace MQTTnet.Tests.Server
.Build(); .Build();
mqttServerOptions.TlsEndpointOptions.ClientCertificateRequired = true; mqttServerOptions.TlsEndpointOptions.ClientCertificateRequired = true;
_server = mqttFactory.CreateMqttServer(mqttServerOptions); _server = mqttFactory.CreateMqttServer(mqttServerOptions);
await _server.StartAsync(); return _server.StartAsync();
} }
} }
class HotSwappableClientCertProvider : IMqttClientCertificatesProvider class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable
{ {
X509Certificate2Collection _certificates; X509Certificate2Collection _certificates;
ConcurrentBag<X509Certificate2> ServerCerts = new ConcurrentBag<X509Certificate2>(); ConcurrentBag<X509Certificate2> _serverCerts = new ConcurrentBag<X509Certificate2>();
public HotSwappableClientCertProvider() public HotSwappableClientCertProvider()
{ {
_certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2"));
} }
public void ClearServerCerts() public void Dispose()
{ {
ServerCerts = new ConcurrentBag<X509Certificate2>(); if (_certificates != null)
{
foreach (var certs in _certificates)
{
#if !NET452
certs.Dispose();
#endif
}
}
} }
public X509CertificateCollection GetCertificates() public X509CertificateCollection GetCertificates()
@@ -339,18 +339,17 @@ namespace MQTTnet.Tests.Server
public void HotSwapCert() public void HotSwapCert()
{ {
var newCert = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2"));
var oldCerts = Interlocked.Exchange(ref _certificates, newCert);
} }
public void InstallNewServerCert(X509Certificate2 serverCert) public void InstallNewServerCert(X509Certificate2 serverCert)
{ {
ServerCerts.Add(serverCert); _serverCerts.Add(serverCert);
} }
public bool OnCertificateValidation(MqttClientCertificateValidationEventArgs certContext) public bool OnCertificateValidation(MqttClientCertificateValidationEventArgs certContext)
{ {
var serverCerts = ServerCerts.ToArray(); var serverCerts = _serverCerts.ToArray();
var providedCert = certContext.Certificate.GetRawCertData(); var providedCert = certContext.Certificate.GetRawCertData();
for (int i = 0, n = serverCerts.Length; i < n; i++) for (int i = 0, n = serverCerts.Length; i < n; i++)
@@ -365,36 +364,18 @@ namespace MQTTnet.Tests.Server
return false; return false;
} }
void Dispose()
{
if (_certificates != null)
{
foreach (var certs in _certificates)
{
#if !NET452
certs.Dispose();
#endif
}
}
}
} }
class HotSwappableServerCertProvider : ICertificateProvider, IDisposable sealed class HotSwappableServerCertProvider : ICertificateProvider, IDisposable
{ {
readonly ConcurrentBag<X509Certificate2> _clientCerts = new ConcurrentBag<X509Certificate2>();
X509Certificate2 _certificate; X509Certificate2 _certificate;
ConcurrentBag<X509Certificate2> ClientCerts = new ConcurrentBag<X509Certificate2>();
public HotSwappableServerCertProvider() public HotSwappableServerCertProvider()
{ {
_certificate = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1"); _certificate = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1");
} }
public void ClearClientCerts()
{
ClientCerts = new ConcurrentBag<X509Certificate2>();
}
public void Dispose() public void Dispose()
{ {
#if !NET452 #if !NET452
@@ -418,12 +399,12 @@ namespace MQTTnet.Tests.Server
public void InstallNewClientCert(X509Certificate2 certificate) public void InstallNewClientCert(X509Certificate2 certificate)
{ {
ClientCerts.Add(certificate); _clientCerts.Add(certificate);
} }
public bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) public bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{ {
var serverCerts = ClientCerts.ToArray(); var serverCerts = _clientCerts.ToArray();
var providedCert = certificate.GetRawCertData(); var providedCert = certificate.GetRawCertData();
for (int i = 0, n = serverCerts.Length; i < n; i++) for (int i = 0, n = serverCerts.Length; i < n; i++)
@@ -8,6 +8,7 @@ using MQTTnet.Client;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Tests.Server namespace MQTTnet.Tests.Server
{ {
+1 -1
View File
@@ -283,7 +283,7 @@ namespace MQTTnet.Tests.Server
server.InterceptingPublishAsync += e => server.InterceptingPublishAsync += e =>
{ {
e.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty); e.ApplicationMessage.PayloadSegment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty));
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
+9 -11
View File
@@ -101,7 +101,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic1", Topic = "TestTopic1",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
}); });
await testEnvironment.Server.InjectApplicationMessage( await testEnvironment.Server.InjectApplicationMessage(
@@ -109,7 +109,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic1", Topic = "TestTopic1",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
})); }));
certificateProvider.CurrentCertificate = CreateCertificate(secondOid); certificateProvider.CurrentCertificate = CreateCertificate(secondOid);
@@ -137,7 +137,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic2", Topic = "TestTopic2",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
}); });
await testEnvironment.Server.InjectApplicationMessage( await testEnvironment.Server.InjectApplicationMessage(
@@ -145,7 +145,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic2", Topic = "TestTopic2",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
})); }));
// Ensure first client still works // Ensure first client still works
@@ -153,7 +153,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic1", Topic = "TestTopic1",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
}); });
await testEnvironment.Server.InjectApplicationMessage( await testEnvironment.Server.InjectApplicationMessage(
@@ -161,7 +161,7 @@ namespace MQTTnet.Tests.Server
new MqttApplicationMessage new MqttApplicationMessage
{ {
Topic = "TestTopic1", Topic = "TestTopic1",
Payload = new byte[] { 1, 2, 3, 4 } PayloadSegment = new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 })
})); }));
await Task.Delay(1000); await Task.Delay(1000);
@@ -178,12 +178,10 @@ namespace MQTTnet.Tests.Server
var clientOptionsBuilder = testEnvironment.Factory.CreateClientOptionsBuilder(); var clientOptionsBuilder = testEnvironment.Factory.CreateClientOptionsBuilder();
clientOptionsBuilder.WithClientId(Guid.NewGuid().ToString()) clientOptionsBuilder.WithClientId(Guid.NewGuid().ToString())
.WithTcpServer("localhost", 8883) .WithTcpServer("localhost", 8883)
.WithTls( .WithTlsOptions(
tls => o =>
{ {
tls.UseTls = true; o.WithSslProtocols(SslProtocols.Tls12).WithCertificateValidationHandler(certValidator);
tls.SslProtocol = SslProtocols.Tls12;
tls.CertificateValidationHandler = certValidator;
}); });
var clientOptions = clientOptionsBuilder.Build(); var clientOptions = clientOptionsBuilder.Build();
+17 -4
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Extensions.TopicTemplate;
namespace MQTTnet.Tests namespace MQTTnet.Tests
{ {
@@ -31,31 +32,43 @@ namespace MQTTnet.Tests
{ {
maxNumLevel3Topics = 1; maxNumLevel3Topics = 1;
} }
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
for (var p = 0; p < numPublishers; ++p) for (var p = 0; p < numPublishers; ++p)
{ {
int publisherTopicCount = 0; int publisherTopicCount = 0;
var publisherName = "pub" + p; var publisherName = "pub" + p;
var publisherTemplate = baseTemplate
.WithParameter("publisher", publisherName);
for (var l1 = 0; l1 < numLevel1Topics; ++l1) for (var l1 = 0; l1 < numLevel1Topics; ++l1)
{ {
var l1Template = publisherTemplate.WithParameter("building", "building" + (l1 + 1));
for (var l2 = 0; l2 < numLevel2Topics; ++l2) for (var l2 = 0; l2 < numLevel2Topics; ++l2)
{ {
var l2Template = l1Template.WithParameter("level", "level" + (l2 + 1));
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
{ {
if (publisherTopicCount >= numTopicsPerPublisher) if (publisherTopicCount >= numTopicsPerPublisher)
break; break;
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1); var l3Template = l2Template
AddPublisherTopic(publisherName, topic, topicsByPublisher); .WithParameter("sensor", "sensor" + (l3 + 1));
AddPublisherTopic(publisherName, l3Template.TopicFilter, topicsByPublisher);
if (l2 == 0) if (l2 == 0)
{ {
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1); var singleWildcardTopic = l1Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
} }
if ((l1 == 0) && (l3 == 0)) if ((l1 == 0) && (l3 == 0))
{ {
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l2 + 1); var multiWildcardTopic = publisherTemplate
.WithParameter("level", "level" + (l2 + 1))
.TopicFilter;
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
} }
@@ -25,8 +25,6 @@ namespace MQTTnet.Adapter
long BytesReceived { get; } long BytesReceived { get; }
bool IsReadingPacket { get; }
Task ConnectAsync(CancellationToken cancellationToken); Task ConnectAsync(CancellationToken cancellationToken);
Task DisconnectAsync(CancellationToken cancellationToken); Task DisconnectAsync(CancellationToken cancellationToken);
@@ -14,6 +14,7 @@ namespace MQTTnet.Adapter
Func<IMqttChannelAdapter, Task> ClientHandler { get; set; } Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
Task StartAsync(MqttServerOptions options, IMqttNetLogger logger); Task StartAsync(MqttServerOptions options, IMqttNetLogger logger);
Task StopAsync(); Task StopAsync();
} }
} }
+64 -49
View File
@@ -26,9 +26,7 @@ namespace MQTTnet.Adapter
readonly IMqttChannel _channel; readonly IMqttChannel _channel;
readonly byte[] _fixedHeaderBuffer = new byte[2]; readonly byte[] _fixedHeaderBuffer = new byte[2];
readonly MqttNetSourceLogger _logger; readonly MqttNetSourceLogger _logger;
readonly byte[] _singleByteBuffer = new byte[1]; readonly byte[] _singleByteBuffer = new byte[1];
readonly AsyncLock _syncRoot = new AsyncLock(); readonly AsyncLock _syncRoot = new AsyncLock();
Statistics _statistics; // mutable struct, don't make readonly! Statistics _statistics; // mutable struct, don't make readonly!
@@ -47,6 +45,8 @@ namespace MQTTnet.Adapter
_logger = logger.WithSource(nameof(MqttChannelAdapter)); _logger = logger.WithSource(nameof(MqttChannelAdapter));
} }
public bool AllowPacketFragmentation { get; set; } = true;
public long BytesReceived => Volatile.Read(ref _statistics._bytesReceived); public long BytesReceived => Volatile.Read(ref _statistics._bytesReceived);
public long BytesSent => Volatile.Read(ref _statistics._bytesSent); public long BytesSent => Volatile.Read(ref _statistics._bytesSent);
@@ -55,16 +55,12 @@ namespace MQTTnet.Adapter
public string Endpoint => _channel.Endpoint; public string Endpoint => _channel.Endpoint;
public bool IsReadingPacket { get; private set; }
public bool IsSecureConnection => _channel.IsSecureConnection; public bool IsSecureConnection => _channel.IsSecureConnection;
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public MqttPacketInspector PacketInspector { get; set; } public MqttPacketInspector PacketInspector { get; set; }
public bool AllowPacketFragmentation { get; set; } = true;
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -78,12 +74,30 @@ namespace MQTTnet.Adapter
* block forever. Even a cancellation token is not supported properly. * block forever. Even a cancellation token is not supported properly.
*/ */
var connectTask = _channel.ConnectAsync(cancellationToken);
var timeout = new TaskCompletionSource<object>(); var timeout = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => timeout.TrySetResult(null))) using (cancellationToken.Register(() => timeout.TrySetResult(null)))
{ {
var connectTask = Task.Run(
async () =>
{
try
{
await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
// If the timeout is already reached the exception is no longer of interest and
// must be catched. Otherwise it will arrive at the TaskScheduler.UnobservedTaskException.
if (!timeout.Task.IsCompleted)
{
throw;
}
}
},
CancellationToken.None);
await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false); await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false);
if (timeout.Task.IsCompleted && !connectTask.IsCompleted) if (timeout.Task.IsCompleted && !connectTask.IsCompleted)
{ {
throw new OperationCanceledException("MQTT connect canceled.", cancellationToken); throw new OperationCanceledException("MQTT connect canceled.", cancellationToken);
@@ -127,7 +141,8 @@ namespace MQTTnet.Adapter
try try
{ {
PacketInspector?.BeginReceivePacket(); var localPacketInspector = PacketInspector;
localPacketInspector?.BeginReceivePacket();
ReceivedMqttPacket receivedPacket; ReceivedMqttPacket receivedPacket;
var receivedPacketTask = ReceiveAsync(cancellationToken); var receivedPacketTask = ReceiveAsync(cancellationToken);
@@ -145,7 +160,10 @@ namespace MQTTnet.Adapter
return null; return null;
} }
PacketInspector?.EndReceivePacket(); if (localPacketInspector != null)
{
await localPacketInspector.EndReceivePacket().ConfigureAwait(false);
}
Interlocked.Add(ref _statistics._bytesSent, receivedPacket.TotalLength); Interlocked.Add(ref _statistics._bytesSent, receivedPacket.TotalLength);
@@ -201,10 +219,15 @@ namespace MQTTnet.Adapter
try try
{ {
var packetBuffer = PacketFormatterAdapter.Encode(packet); var packetBuffer = PacketFormatterAdapter.Encode(packet);
PacketInspector?.BeginSendPacket(packetBuffer);
_logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet); var localPacketInspector = PacketInspector;
if (localPacketInspector != null)
{
await localPacketInspector.BeginSendPacket(packetBuffer).ConfigureAwait(false);
}
_logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet);
if (packetBuffer.Payload.Count == 0 || !AllowPacketFragmentation) if (packetBuffer.Payload.Count == 0 || !AllowPacketFragmentation)
{ {
await _channel.WriteAsync(packetBuffer.Join(), true, cancellationToken).ConfigureAwait(false); await _channel.WriteAsync(packetBuffer.Join(), true, cancellationToken).ConfigureAwait(false);
@@ -373,53 +396,45 @@ namespace MQTTnet.Adapter
return ReceivedMqttPacket.Empty; return ReceivedMqttPacket.Empty;
} }
IsReadingPacket = true; var fixedHeader = readFixedHeaderResult.FixedHeader;
try if (fixedHeader.RemainingLength == 0)
{ {
var fixedHeader = readFixedHeaderResult.FixedHeader; return new ReceivedMqttPacket(fixedHeader.Flags, EmptyBuffer.ArraySegment, 2);
if (fixedHeader.RemainingLength == 0) }
var bodyLength = fixedHeader.RemainingLength;
var body = new byte[bodyLength];
var bodyOffset = 0;
var chunkSize = Math.Min(ReadBufferSize, bodyLength);
do
{
var bytesLeft = body.Length - bodyOffset;
if (chunkSize > bytesLeft)
{ {
return new ReceivedMqttPacket(fixedHeader.Flags, EmptyBuffer.ArraySegment, 2); chunkSize = bytesLeft;
} }
var bodyLength = fixedHeader.RemainingLength; var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false);
var body = new byte[bodyLength];
var bodyOffset = 0; if (cancellationToken.IsCancellationRequested)
var chunkSize = Math.Min(ReadBufferSize, bodyLength);
do
{ {
var bytesLeft = body.Length - bodyOffset; return ReceivedMqttPacket.Empty;
if (chunkSize > bytesLeft) }
{
chunkSize = bytesLeft;
}
var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false); if (readBytes == 0)
{
return ReceivedMqttPacket.Empty;
}
if (cancellationToken.IsCancellationRequested) bodyOffset += readBytes;
{ } while (bodyOffset < bodyLength);
return ReceivedMqttPacket.Empty;
}
if (readBytes == 0) PacketInspector?.FillReceiveBuffer(body);
{
return ReceivedMqttPacket.Empty;
}
bodyOffset += readBytes; var bodySegment = new ArraySegment<byte>(body, 0, bodyLength);
} while (bodyOffset < bodyLength); return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength);
PacketInspector?.FillReceiveBuffer(body);
var bodySegment = new ArraySegment<byte>(body, 0, bodyLength);
return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength);
}
finally
{
IsReadingPacket = false;
}
} }
static bool WrapAndThrowException(Exception exception) static bool WrapAndThrowException(Exception exception)
+30 -25
View File
@@ -4,6 +4,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
@@ -12,16 +13,20 @@ namespace MQTTnet.Adapter
{ {
public sealed class MqttPacketInspector public sealed class MqttPacketInspector
{ {
readonly MqttNetSourceLogger _logger;
readonly AsyncEvent<InspectMqttPacketEventArgs> _asyncEvent; readonly AsyncEvent<InspectMqttPacketEventArgs> _asyncEvent;
readonly MqttNetSourceLogger _logger;
MemoryStream _receivedPacketBuffer; MemoryStream _receivedPacketBuffer;
public MqttPacketInspector(AsyncEvent<InspectMqttPacketEventArgs> asyncEvent, IMqttNetLogger logger) public MqttPacketInspector(AsyncEvent<InspectMqttPacketEventArgs> asyncEvent, IMqttNetLogger logger)
{ {
_asyncEvent = asyncEvent ?? throw new ArgumentNullException(nameof(asyncEvent)); _asyncEvent = asyncEvent ?? throw new ArgumentNullException(nameof(asyncEvent));
if (logger == null) throw new ArgumentNullException(nameof(logger)); if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger.WithSource(nameof(MqttPacketInspector)); _logger = logger.WithSource(nameof(MqttPacketInspector));
} }
@@ -36,28 +41,15 @@ namespace MQTTnet.Adapter
{ {
_receivedPacketBuffer = new MemoryStream(); _receivedPacketBuffer = new MemoryStream();
} }
_receivedPacketBuffer?.SetLength(0); _receivedPacketBuffer?.SetLength(0);
} }
public void EndReceivePacket() public Task BeginSendPacket(MqttPacketBuffer buffer)
{ {
if (!_asyncEvent.HasHandlers) if (!_asyncEvent.HasHandlers)
{ {
return; return CompletedTask.Instance;
}
var buffer = _receivedPacketBuffer.ToArray();
_receivedPacketBuffer.SetLength(0);
InspectPacket(buffer, MqttPacketFlowDirection.Inbound);
}
public void BeginSendPacket(MqttPacketBuffer buffer)
{
if (!_asyncEvent.HasHandlers)
{
return;
} }
// Create a copy of the actual packet so that the inspector gets no access // Create a copy of the actual packet so that the inspector gets no access
@@ -65,7 +57,20 @@ namespace MQTTnet.Adapter
// intended for debugging etc. so that this is OK. // intended for debugging etc. so that this is OK.
var bufferCopy = buffer.ToArray(); var bufferCopy = buffer.ToArray();
InspectPacket(bufferCopy, MqttPacketFlowDirection.Outbound); return InspectPacket(bufferCopy, MqttPacketFlowDirection.Outbound);
}
public Task EndReceivePacket()
{
if (!_asyncEvent.HasHandlers)
{
return CompletedTask.Instance;
}
var buffer = _receivedPacketBuffer.ToArray();
_receivedPacketBuffer.SetLength(0);
return InspectPacket(buffer, MqttPacketFlowDirection.Inbound);
} }
public void FillReceiveBuffer(byte[] buffer) public void FillReceiveBuffer(byte[] buffer)
@@ -74,16 +79,16 @@ namespace MQTTnet.Adapter
{ {
return; return;
} }
_receivedPacketBuffer?.Write(buffer, 0, buffer.Length); _receivedPacketBuffer?.Write(buffer, 0, buffer.Length);
} }
void InspectPacket(byte[] buffer, MqttPacketFlowDirection direction) async Task InspectPacket(byte[] buffer, MqttPacketFlowDirection direction)
{ {
try try
{ {
var eventArgs = new InspectMqttPacketEventArgs(direction, buffer); var eventArgs = new InspectMqttPacketEventArgs(direction, buffer);
_asyncEvent.InvokeAsync(eventArgs).GetAwaiter().GetResult(); await _asyncEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
} }
catch (Exception exception) catch (Exception exception)
{ {
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Packets; using MQTTnet.Packets;
@@ -11,8 +12,9 @@ namespace MQTTnet.Client
{ {
public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException
{ {
public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket) public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket, Exception innerExcpetion = null) : base(
: base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.") $"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.",
innerExcpetion)
{ {
ReasonCode = disconnectPacket.ReasonCode; ReasonCode = disconnectPacket.ReasonCode;
SessionExpiryInterval = disconnectPacket.SessionExpiryInterval; SessionExpiryInterval = disconnectPacket.SessionExpiryInterval;
@@ -23,12 +25,12 @@ namespace MQTTnet.Client
public MqttDisconnectReasonCode? ReasonCode { get; } public MqttDisconnectReasonCode? ReasonCode { get; }
public uint? SessionExpiryInterval { get; }
public string ReasonString { get; } public string ReasonString { get; }
public List<MqttUserProperty> UserProperties { get; }
public string ServerReference { get; } public string ServerReference { get; }
public uint? SessionExpiryInterval { get; }
public List<MqttUserProperty> UserProperties { get; }
} }
} }
+154 -127
View File
@@ -48,6 +48,7 @@ namespace MQTTnet.Client
Task _packetReceiverTask; Task _packetReceiverTask;
AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue; AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue;
Task _publishPacketReceiverTask; Task _publishPacketReceiverTask;
MqttDisconnectPacket _unexpectedDisconnectPacket;
public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger)
{ {
@@ -123,6 +124,8 @@ namespace MQTTnet.Client
var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_events.InspectPacketEvent, _rootLogger), _rootLogger); var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_events.InspectPacketEvent, _rootLogger), _rootLogger);
_adapter = adapter ?? throw new InvalidOperationException("The adapter factory did not provide an adapter."); _adapter = adapter ?? throw new InvalidOperationException("The adapter factory did not provide an adapter.");
_unexpectedDisconnectPacket = null;
if (cancellationToken.CanBeCanceled) if (cancellationToken.CanBeCanceled)
{ {
connectResult = await ConnectInternal(adapter, cancellationToken).ConfigureAwait(false); connectResult = await ConnectInternal(adapter, cancellationToken).ConfigureAwait(false);
@@ -215,7 +218,7 @@ namespace MQTTnet.Client
} }
// Sending the DISCONNECT may fail due to connection issues. The resulting exception // Sending the DISCONNECT may fail due to connection issues. The resulting exception
// must be throw to let the caller know that the disconnect is not a clean one. // must be thrown to let the caller know that the disconnect was not clean.
var disconnectPacket = MqttPacketFactories.Disconnect.Create(options); var disconnectPacket = MqttPacketFactories.Disconnect.Create(options);
if (cancellationToken.CanBeCanceled) if (cancellationToken.CanBeCanceled)
@@ -239,7 +242,7 @@ namespace MQTTnet.Client
public async Task PingAsync(CancellationToken cancellationToken = default) public async Task PingAsync(CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed(); ThrowIfDisposed();
ThrowIfNotConnected(); ThrowIfNotConnected();
@@ -309,6 +312,7 @@ namespace MQTTnet.Client
AuthenticationMethod = Options.AuthenticationMethod, AuthenticationMethod = Options.AuthenticationMethod,
AuthenticationData = data.AuthenticationData, AuthenticationData = data.AuthenticationData,
ReasonString = data.ReasonString, ReasonString = data.ReasonString,
ReasonCode = data.ReasonCode,
UserProperties = data.UserProperties UserProperties = data.UserProperties
}; };
@@ -329,7 +333,7 @@ namespace MQTTnet.Client
ThrowIfDisposed(); ThrowIfDisposed();
ThrowIfNotConnected(); ThrowIfNotConnected();
if (Options.ValidateFeatures) if (Options.ValidateFeatures)
{ {
MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion); MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion);
@@ -405,29 +409,37 @@ namespace MQTTnet.Client
Task AcknowledgeReceivedPublishPacket(MqttApplicationMessageReceivedEventArgs eventArgs, CancellationToken cancellationToken) Task AcknowledgeReceivedPublishPacket(MqttApplicationMessageReceivedEventArgs eventArgs, CancellationToken cancellationToken)
{ {
if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) switch (eventArgs.PublishPacket.QualityOfServiceLevel)
{ {
// no response required case MqttQualityOfServiceLevel.AtMostOnce:
}
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
{
if (!eventArgs.ProcessingFailed)
{ {
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs); // no response required
return Send(pubAckPacket, cancellationToken); break;
} }
} case MqttQualityOfServiceLevel.AtLeastOnce:
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
{
if (!eventArgs.ProcessingFailed)
{ {
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs); if (!eventArgs.ProcessingFailed)
return Send(pubRecPacket, cancellationToken); {
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs);
return Send(pubAckPacket, cancellationToken);
}
break;
}
case MqttQualityOfServiceLevel.ExactlyOnce:
{
if (!eventArgs.ProcessingFailed)
{
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs);
return Send(pubRecPacket, cancellationToken);
}
break;
}
default:
{
throw new MqttProtocolViolationException("Received a not supported QoS level.");
} }
}
else
{
throw new MqttProtocolViolationException("Received a not supported QoS level.");
} }
return CompletedTask.Instance; return CompletedTask.Instance;
@@ -444,21 +456,25 @@ namespace MQTTnet.Client
var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false);
if (receivedPacket is MqttConnAckPacket connAckPacket) switch (receivedPacket)
{ {
result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); case MqttConnAckPacket connAckPacket:
} {
else if (receivedPacket is MqttAuthPacket) result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion);
{ break;
throw new NotSupportedException("Extended authentication handler is not yet supported"); }
} case MqttAuthPacket _:
else if (receivedPacket == null) {
{ throw new NotSupportedException("Extended authentication handler is not yet supported");
throw new MqttCommunicationException("Connection closed."); }
} case null:
else {
{ throw new MqttCommunicationException("Connection closed.");
throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket})."); }
default:
{
throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket}).");
}
} }
} }
catch (Exception exception) catch (Exception exception)
@@ -588,8 +604,8 @@ namespace MQTTnet.Client
_disconnectUserProperties, _disconnectUserProperties,
exception); exception);
// This handler must be executed in a new thread because otherwise a dead lock may happen // This handler must be executed in a new thread to prevent a deadlock
// when trying to reconnect in that handler etc. // that may occur when attempting to reconnect within that handler, etc.
Task.Run(() => _events.DisconnectedEvent.InvokeAsync(eventArgs)).RunInBackground(_logger); Task.Run(() => _events.DisconnectedEvent.InvokeAsync(eventArgs)).RunInBackground(_logger);
} }
} }
@@ -598,12 +614,9 @@ namespace MQTTnet.Client
{ {
var clientWasConnected = IsConnected; var clientWasConnected = IsConnected;
if (!DisconnectIsPendingOrFinished()) return !DisconnectIsPendingOrFinished() ?
{ DisconnectCore(sender, exception, connectResult, clientWasConnected)
return DisconnectCore(sender, exception, connectResult, clientWasConnected); : CompletedTask.Instance;
}
return CompletedTask.Instance;
} }
bool DisconnectIsPendingOrFinished() bool DisconnectIsPendingOrFinished()
@@ -656,24 +669,19 @@ namespace MQTTnet.Client
Task OnConnected(MqttClientConnectResult connectResult) Task OnConnected(MqttClientConnectResult connectResult)
{ {
if (_events.ConnectedEvent.HasHandlers) if (!_events.ConnectedEvent.HasHandlers)
{ {
var eventArgs = new MqttClientConnectedEventArgs(connectResult); return CompletedTask.Instance;
return _events.ConnectedEvent.InvokeAsync(eventArgs);
} }
return CompletedTask.Instance; var eventArgs = new MqttClientConnectedEventArgs(connectResult);
return _events.ConnectedEvent.InvokeAsync(eventArgs);
} }
Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket) Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket)
{ {
var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler; var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler;
if (extendedAuthenticationExchangeHandler != null) return extendedAuthenticationExchangeHandler != null ? extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)) : CompletedTask.Instance;
{
return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this));
}
return CompletedTask.Instance;
} }
Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket)
@@ -681,9 +689,10 @@ namespace MQTTnet.Client
_disconnectReason = (int)disconnectPacket.ReasonCode; _disconnectReason = (int)disconnectPacket.ReasonCode;
_disconnectReasonString = disconnectPacket.ReasonString; _disconnectReasonString = disconnectPacket.ReasonString;
_disconnectUserProperties = disconnectPacket.UserProperties; _disconnectUserProperties = disconnectPacket.UserProperties;
_unexpectedDisconnectPacket = disconnectPacket;
// Also dispatch disconnect to waiting threads to generate a proper exception. // Also dispatch disconnect to waiting threads to generate a proper exception.
_packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket)); _packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket, null));
return DisconnectInternal(_packetReceiverTask, null, null); return DisconnectInternal(_packetReceiverTask, null, null);
} }
@@ -708,27 +717,30 @@ namespace MQTTnet.Client
await eventArgs.AcknowledgeAsync(cancellationToken).ConfigureAwait(false); await eventArgs.AcknowledgeAsync(cancellationToken).ConfigureAwait(false);
} }
} }
catch (ObjectDisposedException)
{
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Error(exception, "Error while handling application message."); _logger.Error(exception, "Error while handling application message");
} }
} }
} }
Task ProcessReceivedPubRecPacket(MqttPubRecPacket pubRecPacket, CancellationToken cancellationToken) Task ProcessReceivedPubRecPacket(MqttPubRecPacket pubRecPacket, CancellationToken cancellationToken)
{ {
if (!_packetDispatcher.TryDispatch(pubRecPacket)) if (_packetDispatcher.TryDispatch(pubRecPacket))
{ {
// The packet is unknown. Probably due to a restart of the client. return CompletedTask.Instance;
// So wen send this to the server to trigger a full resend of the message.
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
return Send(pubRelPacket, cancellationToken);
} }
return CompletedTask.Instance; // The packet is unknown, probably due to a restart of the client.
// So we send this to the server to trigger a full resend of the message.
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
return Send(pubRelPacket, cancellationToken);
} }
Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken)
@@ -747,10 +759,27 @@ namespace MQTTnet.Client
async Task<MqttClientPublishResult> PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) async Task<MqttClientPublishResult> PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
{ {
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] try
await Send(publishPacket, cancellationToken).ConfigureAwait(false); {
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier]
await Send(publishPacket, cancellationToken).ConfigureAwait(false);
return MqttClientResultFactory.PublishResult.Create(null); return MqttClientResultFactory.PublishResult.Create(null);
}
catch (Exception exception)
{
// We have to check if the server has sent a disconnect packet in response to the published message.
// Since we are in QoS 0 we do not get a direct response via an PUBACK packet and thus basically no
// feedback at all.
var localUnexpectedDisconnectPacket = _unexpectedDisconnectPacket;
if (localUnexpectedDisconnectPacket != null)
{
throw new MqttClientUnexpectedDisconnectReceivedException(localUnexpectedDisconnectPacket, exception);
}
throw;
}
} }
async Task<MqttClientPublishResult> PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) async Task<MqttClientPublishResult> PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
@@ -787,7 +816,7 @@ namespace MQTTnet.Client
{ {
try try
{ {
_logger.Verbose("Start receiving packets."); _logger.Verbose("Start receiving packets");
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
@@ -825,20 +854,21 @@ namespace MQTTnet.Client
} }
else if (exception is MqttCommunicationException) else if (exception is MqttCommunicationException)
{ {
_logger.Warning(exception, "Communication error while receiving packets."); _logger.Warning(exception, "Communication error while receiving packets");
} }
else else
{ {
_logger.Error(exception, "Error while receiving packets."); _logger.Error(exception, "Error while receiving packets");
} }
_packetDispatcher.FailAll(exception); // The packet dispatcher is set to null when the client is being disposed so it may already being gone!
_packetDispatcher?.FailAll(exception);
await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false); await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false);
} }
finally finally
{ {
_logger.Verbose("Stopped receiving packets."); _logger.Verbose("Stopped receiving packets");
} }
} }
@@ -860,7 +890,7 @@ namespace MQTTnet.Client
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Warning(exception, "Error when sending request packet ({0}).", requestPacket.GetType().Name); _logger.Warning(exception, "Error when sending {0} request packet", requestPacket.GetType().Name);
packetAwaitable.Fail(exception); packetAwaitable.Fail(exception);
} }
@@ -872,7 +902,7 @@ namespace MQTTnet.Client
{ {
if (exception is MqttCommunicationTimedOutException) if (exception is MqttCommunicationTimedOutException)
{ {
_logger.Warning("Timeout while waiting for response packet ({0}).", typeof(TResponsePacket).Name); _logger.Warning("Timeout while waiting for {0} response packet", typeof(TResponsePacket).Name);
} }
throw; throw;
@@ -925,7 +955,7 @@ namespace MQTTnet.Client
static void ThrowNotConnected() static void ThrowNotConnected()
{ {
throw new MqttCommunicationException("The client is not connected."); throw new MqttClientNotConnectedException();
} }
void TryInitiateDisconnect() void TryInitiateDisconnect()
@@ -938,7 +968,7 @@ namespace MQTTnet.Client
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Warning(exception, "Error while initiating disconnect."); _logger.Warning(exception, "Error while initiating disconnect");
} }
} }
} }
@@ -947,39 +977,36 @@ namespace MQTTnet.Client
{ {
try try
{ {
if (packet is MqttPublishPacket publishPacket) switch (packet)
{ {
EnqueueReceivedPublishPacket(publishPacket); case MqttPublishPacket publishPacket:
} EnqueueReceivedPublishPacket(publishPacket);
else if (packet is MqttPubRecPacket pubRecPacket) break;
{ case MqttPubRecPacket pubRecPacket:
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false); await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
} break;
else if (packet is MqttPubRelPacket pubRelPacket) case MqttPubRelPacket pubRelPacket:
{ await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false);
await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false); break;
} case MqttDisconnectPacket disconnectPacket:
else if (packet is MqttDisconnectPacket disconnectPacket) await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false);
{ break;
await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false); case MqttAuthPacket authPacket:
} await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false);
else if (packet is MqttAuthPacket authPacket) break;
{ case MqttPingRespPacket _:
await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false); _packetDispatcher.TryDispatch(packet);
} break;
else if (packet is MqttPingRespPacket) case MqttPingReqPacket _:
{ throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a client to the server only.");
_packetDispatcher.TryDispatch(packet); default:
}
else if (packet is MqttPingReqPacket)
{
throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a Client to the Server only.");
}
else
{
if (!_packetDispatcher.TryDispatch(packet))
{ {
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); if (!_packetDispatcher.TryDispatch(packet))
{
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time.");
}
break;
} }
} }
} }
@@ -990,19 +1017,20 @@ namespace MQTTnet.Client
return; return;
} }
if (exception is OperationCanceledException) switch (exception)
{ {
} case OperationCanceledException _:
else if (exception is MqttCommunicationException) break;
{ case MqttCommunicationException _:
_logger.Warning(exception, "Communication error while receiving packets."); _logger.Warning(exception, "Communication error while receiving packets");
} break;
else default:
{ _logger.Error(exception, "Error while processing received {0} packet", packet.GetType().Name);
_logger.Error(exception, $"Error while processing received packet ({packet.GetType().Name})."); break;
} }
_packetDispatcher.FailAll(exception); // The packet dispatcher may already be gone due to disconnect etc.!
_packetDispatcher?.FailAll(exception);
await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false); await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false);
} }
@@ -1012,7 +1040,7 @@ namespace MQTTnet.Client
{ {
try try
{ {
_logger.Verbose("Start sending keep alive packets."); _logger.Verbose("Start sending keep alive packets");
var keepAlivePeriod = Options.KeepAlivePeriod; var keepAlivePeriod = Options.KeepAlivePeriod;
@@ -1044,25 +1072,24 @@ namespace MQTTnet.Client
return; return;
} }
if (exception is OperationCanceledException) switch (exception)
{ {
return; case OperationCanceledException _:
} return;
else if (exception is MqttCommunicationException) case MqttCommunicationException _:
{ _logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets."); break;
} default:
else _logger.Error(exception, "Error exception while sending/receiving keep alive packets");
{ break;
_logger.Error(exception, "Error exception while sending/receiving keep alive packets.");
} }
await DisconnectInternal(_keepAlivePacketsSenderTask, exception, null).ConfigureAwait(false); await DisconnectInternal(_keepAlivePacketsSenderTask, exception, null).ConfigureAwait(false);
} }
finally finally
{ {
_logger.Verbose("Stopped sending keep alive packets."); _logger.Verbose("Stopped sending keep alive packets");
} }
} }
} }
} }
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Security.Cryptography.X509Certificates;
namespace MQTTnet.Client
{
public sealed class MqttClientCertificateSelectionEventArgs : EventArgs
{
public MqttClientCertificateSelectionEventArgs(
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers,
MqttClientTcpOptions tcpOptions)
{
TargetHost = targetHost;
LocalCertificates = localCertificates;
RemoveCertificate = remoteCertificate;
AcceptableIssuers = acceptableIssuers;
TcpOptions = tcpOptions ?? throw new ArgumentNullException(nameof(tcpOptions));
}
public string[] AcceptableIssuers { get; }
public X509CertificateCollection LocalCertificates { get; }
public X509Certificate RemoveCertificate { get; }
public string TargetHost { get; }
public MqttClientTcpOptions TcpOptions { get; }
}
}
@@ -10,11 +10,20 @@ namespace MQTTnet.Client
{ {
public sealed class MqttClientCertificateValidationEventArgs : EventArgs public sealed class MqttClientCertificateValidationEventArgs : EventArgs
{ {
public X509Certificate Certificate { get; set; } public MqttClientCertificateValidationEventArgs(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, IMqttClientChannelOptions clientOptions)
{
Certificate = certificate;
Chain = chain;
SslPolicyErrors = sslPolicyErrors;
ClientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
}
public X509Chain Chain { get; set; } public X509Certificate Certificate { get; }
public X509Chain Chain { get; }
public IMqttClientChannelOptions ClientOptions { get; }
public IMqttClientChannelOptions ClientOptions { get; set; }
#if NET452 || NET461 || NET48 #if NET452 || NET461 || NET48
/// <summary> /// <summary>
/// Can be a host string name or an object derived from WebRequest. /// Can be a host string name or an object derived from WebRequest.
@@ -22,6 +31,6 @@ namespace MQTTnet.Client
public object Sender { get; set; } public object Sender { get; set; }
#endif #endif
public SslPolicyErrors SslPolicyErrors { get; set; } public SslPolicyErrors SslPolicyErrors { get; }
} }
} }
@@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#pragma warning disable CS0612 // Type or member is obsolete
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text; using System.Text;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Packets; using MQTTnet.Packets;
@@ -15,8 +19,10 @@ namespace MQTTnet.Client
public sealed class MqttClientOptionsBuilder public sealed class MqttClientOptionsBuilder
{ {
readonly MqttClientOptions _options = new MqttClientOptions(); readonly MqttClientOptions _options = new MqttClientOptions();
int? _port;
[Obsolete] MqttClientWebSocketProxyOptions _proxyOptions; [Obsolete] MqttClientWebSocketProxyOptions _proxyOptions;
EndPoint _remoteEndPoint;
MqttClientTcpOptions _tcpOptions; MqttClientTcpOptions _tcpOptions;
MqttClientTlsOptions _tlsOptions; MqttClientTlsOptions _tlsOptions;
@@ -37,7 +43,7 @@ namespace MQTTnet.Client
// on the order of called methods from the builder). // on the order of called methods from the builder).
// The builder prefers the explicitly set TLS options! // The builder prefers the explicitly set TLS options!
var tlsOptions = _tlsOptions ?? _tcpOptions?.TlsOptions; var tlsOptions = _tlsOptions ?? _tcpOptions?.TlsOptions;
if (_tlsParameters != null) if (_tlsParameters != null)
{ {
if (_tlsParameters?.UseTls == true) if (_tlsParameters?.UseTls == true)
@@ -61,6 +67,46 @@ namespace MQTTnet.Client
if (_tcpOptions != null) if (_tcpOptions != null)
{ {
_tcpOptions.TlsOptions = tlsOptions; _tcpOptions.TlsOptions = tlsOptions;
if (_remoteEndPoint == null)
{
throw new ArgumentException("No endpoint is set.");
}
if (_remoteEndPoint is DnsEndPoint dns)
{
if (dns.Port == 0)
{
if (_port.HasValue)
{
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value, dns.AddressFamily);
}
else
{
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure, dns.AddressFamily);
}
}
}
if (_remoteEndPoint is IPEndPoint ip)
{
if (ip.Port == 0)
{
if (_port.HasValue)
{
_remoteEndPoint = new IPEndPoint(ip.Address, _port.Value);
}
else
{
_remoteEndPoint = new IPEndPoint(ip.Address, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
}
}
}
if (_tcpOptions.RemoteEndpoint == null)
{
_tcpOptions.RemoteEndpoint = _remoteEndPoint;
}
} }
else if (_webSocketOptions != null) else if (_webSocketOptions != null)
{ {
@@ -84,6 +130,12 @@ namespace MQTTnet.Client
return _options; return _options;
} }
public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily)
{
_tcpOptions.AddressFamily = addressFamily;
return this;
}
public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data)
{ {
_options.AuthenticationMethod = method; _options.AuthenticationMethod = method;
@@ -115,7 +167,6 @@ namespace MQTTnet.Client
return this; return this;
} }
[Obsolete("Use WithTcpServer(... configure) or WithWebSocketServer(... configure) instead.")]
public MqttClientOptionsBuilder WithConnectionUri(Uri uri) public MqttClientOptionsBuilder WithConnectionUri(Uri uri)
{ {
if (uri == null) if (uri == null)
@@ -159,7 +210,6 @@ namespace MQTTnet.Client
return this; return this;
} }
[Obsolete("Use WithTcpServer(... configure) or WithWebSocketServer(... configure) instead.")]
public MqttClientOptionsBuilder WithConnectionUri(string uri) public MqttClientOptionsBuilder WithConnectionUri(string uri)
{ {
return WithConnectionUri(new Uri(uri, UriKind.Absolute)); return WithConnectionUri(new Uri(uri, UriKind.Absolute));
@@ -188,6 +238,14 @@ namespace MQTTnet.Client
return this; return this;
} }
public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint)
{
_remoteEndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
_tcpOptions = new MqttClientTcpOptions();
return this;
}
public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler)
{ {
_options.ExtendedAuthenticationExchangeHandler = handler; _options.ExtendedAuthenticationExchangeHandler = handler;
@@ -224,7 +282,7 @@ namespace MQTTnet.Client
} }
/// <summary> /// <summary>
/// The client will not throw an exception when the MQTT server responses with a non success ACK packet. /// The client will not throw an exception when the MQTT server responds with a non success ACK packet.
/// This will become the default behavior in future versions of the library. /// This will become the default behavior in future versions of the library.
/// </summary> /// </summary>
public MqttClientOptionsBuilder WithoutThrowOnNonSuccessfulConnectResponse() public MqttClientOptionsBuilder WithoutThrowOnNonSuccessfulConnectResponse()
@@ -233,6 +291,12 @@ namespace MQTTnet.Client
return this; return this;
} }
public MqttClientOptionsBuilder WithProtocolType(ProtocolType protocolType)
{
_tcpOptions.ProtocolType = protocolType;
return this;
}
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
{ {
if (value == MqttProtocolVersion.Unknown) if (value == MqttProtocolVersion.Unknown)
@@ -303,13 +367,19 @@ namespace MQTTnet.Client
return this; return this;
} }
public MqttClientOptionsBuilder WithTcpServer(string server, int? port = null) public MqttClientOptionsBuilder WithTcpServer(string host, int? port = null, AddressFamily addressFamily = AddressFamily.Unspecified)
{ {
_tcpOptions = new MqttClientTcpOptions if (host == null)
{ {
Server = server, throw new ArgumentNullException(nameof(host));
Port = port }
};
_tcpOptions = new MqttClientTcpOptions();
// The value 0 will be updated when building the options.
// This a backward compatibility feature.
_remoteEndPoint = new DnsEndPoint(host, port ?? 0, addressFamily);
_port = port;
return this; return this;
} }
@@ -2,43 +2,105 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using MQTTnet.Protocol;
namespace MQTTnet.Client namespace MQTTnet.Client
{ {
public sealed class MqttClientTcpOptions : IMqttClientChannelOptions public sealed class MqttClientTcpOptions : IMqttClientChannelOptions
{ {
EndPoint _remoteEndpoint;
public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified; public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified;
/// <summary>
/// Gets the local endpoint (network card) which is used by the client.
/// Set it to _null_ to let the OS select the network card.
/// </summary>
public EndPoint LocalEndpoint { get; set; }
public int BufferSize { get; set; } = 8192; public int BufferSize { get; set; } = 8192;
/// <summary> /// <summary>
/// Gets or sets whether the underlying socket should run in dual mode. /// Gets or sets whether the underlying socket should run in dual mode.
/// Leaving this _null_ will avoid setting this value at socket level. /// Leaving this _null_ will avoid setting this value at socket level.
/// Setting this a value other than _null_ will throw an exception when only IPv4 is supported on the machine. /// Setting this a value other than _null_ will throw an exception when only IPv4 is supported on the machine.
/// </summary> /// </summary>
public bool? DualMode { get; set; } public bool? DualMode { get; set; }
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
public int? Port { get; set; }
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
public string Server { get; set; }
public LingerOption LingerState { get; set; } = new LingerOption(true, 0); public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
/// <summary>
/// Gets the local endpoint (network card) which is used by the client.
/// Set it to _null_ to let the OS select the network card.
/// </summary>
public EndPoint LocalEndpoint { get; set; }
/// <summary>
/// Enables or disables the Nagle algorithm for the socket.
/// This is only supported for TCP.
/// For other protocol types the value is ignored.
/// Default: true
/// </summary>
public bool NoDelay { get; set; } = true; public bool NoDelay { get; set; } = true;
public int? Port { get; set; } /// <summary>
/// The MQTT transport is usually TCP but when using other endpoint types like
public string Server { get; set; } /// unix sockets it must be changed (IP for unix sockets).
/// </summary>
public ProtocolType ProtocolType { get; set; } = ProtocolType.Tcp;
public EndPoint RemoteEndpoint
{
get => _remoteEndpoint;
set
{
_remoteEndpoint = value;
if (_remoteEndpoint is DnsEndPoint dnsEndPoint)
{
Server = dnsEndPoint.Host;
Port = dnsEndPoint.Port;
}
else if (_remoteEndpoint is IPEndPoint ipEndPoint)
{
Server = ipEndPoint.Address.ToString();
Port = ipEndPoint.Port;
}
}
}
public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions();
public override string ToString() public override string ToString()
{ {
return Server + ":" + this.GetPort(); if (RemoteEndpoint != null)
{
return RemoteEndpoint.ToString();
}
if (!string.IsNullOrEmpty(Server))
{
return $"{Server}:{GetPort()}";
}
return string.Empty;
}
int GetPort()
{
if (Port.HasValue)
{
return Port.Value;
}
if (TlsOptions?.UseTls == true)
{
return MqttPorts.Secure;
}
return MqttPorts.Default;
} }
} }
} }
@@ -1,23 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Client
{
public static class MqttClientTcpOptionsExtensions
{
public static int GetPort(this MqttClientTcpOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
if (options.Port.HasValue)
{
return options.Port.Value;
}
return !(options.TlsOptions?.UseTls ?? false) ? 1883 : 8883;
}
}
}
@@ -14,6 +14,8 @@ namespace MQTTnet.Client
{ {
public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; } public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; }
public Func<MqttClientCertificateSelectionEventArgs, X509Certificate> CertificateSelectionHandler { get; set; }
public bool UseTls { get; set; } public bool UseTls { get; set; }
public bool IgnoreCertificateRevocationErrors { get; set; } public bool IgnoreCertificateRevocationErrors { get; set; }
@@ -50,8 +52,13 @@ namespace MQTTnet.Client
#if NET48 || NETCOREAPP3_1_OR_GREATER #if NET48 || NETCOREAPP3_1_OR_GREATER
public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13; public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13;
#else #else
public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/; public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/;
#endif #endif
#if NET7_0_OR_GREATER
public X509Certificate2Collection TrustChain { get; set; }
#endif
} }
} }
@@ -49,6 +49,17 @@ namespace MQTTnet.Client
return this; return this;
} }
public MqttClientTlsOptionsBuilder WithCertificateSelectionHandler(Func<MqttClientCertificateSelectionEventArgs, X509Certificate> certificateSelectionHandler)
{
if (certificateSelectionHandler == null)
{
throw new ArgumentNullException(nameof(certificateSelectionHandler));
}
_tlsOptions.CertificateSelectionHandler = certificateSelectionHandler;
return this;
}
public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates) public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates)
{ {
if (certificates == null) if (certificates == null)
@@ -131,6 +142,15 @@ namespace MQTTnet.Client
_tlsOptions.EncryptionPolicy = encryptionPolicy; _tlsOptions.EncryptionPolicy = encryptionPolicy;
return this; return this;
} }
#endif
#if NET7_0_OR_GREATER
public MqttClientTlsOptionsBuilder WithTrustChain(X509Certificate2Collection chain)
{
_tlsOptions.TrustChain = chain;
return this;
}
#endif #endif
} }
} }
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Exceptions
{
public class MqttClientNotConnectedException : MqttCommunicationException
{
public MqttClientNotConnectedException() : base("The MQTT client is not connected.")
{
}
public MqttClientNotConnectedException(Exception innerException) : base("The MQTT client is not connected.", innerException)
{
}
}
}
@@ -5,45 +5,107 @@
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Formatter namespace MQTTnet.Formatter
{ {
public sealed class MqttDisconnectPacketFactory public sealed class MqttDisconnectPacketFactory
{ {
readonly MqttDisconnectPacket _normalDisconnection = new MqttDisconnectPacket static readonly MqttDisconnectPacket DefaultNormalDisconnection = new MqttDisconnectPacket
{ {
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection ReasonCode = MqttDisconnectReasonCode.NormalDisconnection,
UserProperties = null,
ReasonString = null,
ServerReference = null,
SessionExpiryInterval = 0
};
static readonly MqttDisconnectPacket DefaultServerShuttingDown = new MqttDisconnectPacket
{
ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown,
UserProperties = null,
ReasonString = null,
ServerReference = null,
SessionExpiryInterval = 0
};
static readonly MqttDisconnectPacket DefaultUnspecifiedError = new MqttDisconnectPacket
{
ReasonCode = MqttDisconnectReasonCode.UnspecifiedError,
UserProperties = null,
ReasonString = null,
ServerReference = null,
SessionExpiryInterval = 0
}; };
public MqttDisconnectPacket Create(MqttDisconnectReasonCode reasonCode) public MqttDisconnectPacket Create(MqttDisconnectReasonCode reasonCode)
{ {
if (reasonCode == MqttDisconnectReasonCode.NormalDisconnection) if (reasonCode == MqttDisconnectReasonCode.NormalDisconnection)
{ {
return _normalDisconnection; return DefaultNormalDisconnection;
}
if (reasonCode == MqttDisconnectReasonCode.ServerShuttingDown)
{
return DefaultServerShuttingDown;
}
if (reasonCode == MqttDisconnectReasonCode.UnspecifiedError)
{
return DefaultUnspecifiedError;
} }
return new MqttDisconnectPacket return new MqttDisconnectPacket
{ {
ReasonCode = reasonCode ReasonCode = reasonCode,
UserProperties = null,
ReasonString = null,
ServerReference = null,
SessionExpiryInterval = 0
};
}
public MqttDisconnectPacket Create(MqttServerStopOptions serverStopOptions)
{
if (serverStopOptions == null)
{
return DefaultServerShuttingDown;
}
return Create(serverStopOptions.DefaultClientDisconnectOptions);
}
public MqttDisconnectPacket Create(MqttServerClientDisconnectOptions clientDisconnectOptions)
{
if (clientDisconnectOptions == null)
{
return DefaultNormalDisconnection;
}
return new MqttDisconnectPacket
{
ReasonCode = clientDisconnectOptions.ReasonCode,
UserProperties = clientDisconnectOptions.UserProperties,
ReasonString = clientDisconnectOptions.ReasonString,
ServerReference = clientDisconnectOptions.ServerReference,
SessionExpiryInterval = 0 // TODO: Not yet supported!
}; };
} }
public MqttDisconnectPacket Create(MqttClientDisconnectOptions clientDisconnectOptions) public MqttDisconnectPacket Create(MqttClientDisconnectOptions clientDisconnectOptions)
{ {
var packet = new MqttDisconnectPacket();
if (clientDisconnectOptions == null) if (clientDisconnectOptions == null)
{ {
packet.ReasonCode = MqttDisconnectReasonCode.NormalDisconnection; return DefaultNormalDisconnection;
}
else
{
packet.ReasonCode = (MqttDisconnectReasonCode)clientDisconnectOptions.Reason;
packet.UserProperties = clientDisconnectOptions.UserProperties;
packet.SessionExpiryInterval = clientDisconnectOptions.SessionExpiryInterval;
} }
return packet; return new MqttDisconnectPacket
{
ReasonCode = (MqttDisconnectReasonCode)clientDisconnectOptions.Reason,
UserProperties = clientDisconnectOptions.UserProperties,
SessionExpiryInterval = clientDisconnectOptions.SessionExpiryInterval
};
} }
} }
} }
@@ -68,12 +68,12 @@ namespace MQTTnet.Formatter.V5
MqttPacket DecodeAuthPacket(ArraySegment<byte> body) MqttPacket DecodeAuthPacket(ArraySegment<byte> body)
{ {
ThrowIfBodyIsEmpty(body);
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count); _bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
var packet = new MqttAuthPacket(); var packet = new MqttAuthPacket();
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
// In this case the AUTH has a Remaining Length of 0.
if (_bufferReader.EndOfStream) if (_bufferReader.EndOfStream)
{ {
packet.ReasonCode = MqttAuthenticateReasonCode.Success; packet.ReasonCode = MqttAuthenticateReasonCode.Success;
@@ -220,7 +220,7 @@ namespace MQTTnet.Formatter.V5
var packet = new MqttConnectPacket var packet = new MqttConnectPacket
{ {
// If the Request Problem Information is absent, the value of 1 is used. // If the Request Problem Information is absent, the value of 1 is used.
RequestProblemInformation = true RequestProblemInformation = true
}; };
var protocolName = _bufferReader.ReadString(); var protocolName = _bufferReader.ReadString();
@@ -352,7 +352,16 @@ namespace MQTTnet.Formatter.V5
MqttPacket DecodeDisconnectPacket(ArraySegment<byte> body) MqttPacket DecodeDisconnectPacket(ArraySegment<byte> body)
{ {
ThrowIfBodyIsEmpty(body); // From RFC: 3.14.2.1 Disconnect Reason Code
// Byte 1 in the Variable Header is the Disconnect Reason Code.
// If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used.
if (body.Count == 0)
{
return new MqttDisconnectPacket
{
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection
};
}
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count); _bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
@@ -386,7 +395,7 @@ namespace MQTTnet.Formatter.V5
return packet; return packet;
} }
MqttPacket DecodePubAckPacket(ArraySegment<byte> body) MqttPacket DecodePubAckPacket(ArraySegment<byte> body)
{ {
ThrowIfBodyIsEmpty(body); ThrowIfBodyIsEmpty(body);
@@ -724,7 +733,7 @@ namespace MQTTnet.Formatter.V5
packet.UserProperties = propertiesReader.CollectedUserProperties; packet.UserProperties = propertiesReader.CollectedUserProperties;
packet.ReasonCodes = new List<MqttUnsubscribeReasonCode>(_bufferReader.BytesLeft); packet.ReasonCodes = new List<MqttUnsubscribeReasonCode>(_bufferReader.BytesLeft);
while (!_bufferReader.EndOfStream) while (!_bufferReader.EndOfStream)
{ {
var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte(); var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte();
@@ -770,4 +779,4 @@ namespace MQTTnet.Formatter.V5
} }
} }
} }
} }
@@ -13,7 +13,7 @@ namespace MQTTnet.Formatter.V5
public sealed class MqttV5PacketEncoder public sealed class MqttV5PacketEncoder
{ {
const int FixedHeaderSize = 1; const int FixedHeaderSize = 1;
readonly MqttBufferWriter _bufferWriter; readonly MqttBufferWriter _bufferWriter;
readonly MqttV5PropertiesWriter _propertiesWriter = new MqttV5PropertiesWriter(new MqttBufferWriter(1024, 4096)); readonly MqttV5PropertiesWriter _propertiesWriter = new MqttV5PropertiesWriter(new MqttBufferWriter(1024, 4096));
@@ -64,13 +64,20 @@ namespace MQTTnet.Formatter.V5
byte EncodeAuthPacket(MqttAuthPacket packet) byte EncodeAuthPacket(MqttAuthPacket packet)
{ {
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod); _propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod);
_propertiesWriter.WriteAuthenticationData(packet.AuthenticationData); _propertiesWriter.WriteAuthenticationData(packet.AuthenticationData);
_propertiesWriter.WriteReasonString(packet.ReasonString); _propertiesWriter.WriteReasonString(packet.ReasonString);
_propertiesWriter.WriteUserProperties(packet.UserProperties); _propertiesWriter.WriteUserProperties(packet.UserProperties);
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
// In this case the AUTH has a Remaining Length of 0.
if (packet.ReasonCode == MqttAuthenticateReasonCode.Success && _propertiesWriter.Length == 0)
{
return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Auth);
}
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter); _propertiesWriter.WriteTo(_bufferWriter);
_propertiesWriter.Reset(); _propertiesWriter.Reset();
@@ -279,7 +286,7 @@ namespace MQTTnet.Formatter.V5
_propertiesWriter.WriteReasonString(packet.ReasonString); _propertiesWriter.WriteReasonString(packet.ReasonString);
_propertiesWriter.WriteUserProperties(packet.UserProperties); _propertiesWriter.WriteUserProperties(packet.UserProperties);
if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubAckReasonCode.Success) if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubAckReasonCode.Success)
{ {
_bufferWriter.WriteByte((byte)packet.ReasonCode); _bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter); _propertiesWriter.WriteTo(_bufferWriter);
@@ -375,7 +382,7 @@ namespace MQTTnet.Formatter.V5
_bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier);
if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubRecReasonCode.Success) if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubRecReasonCode.Success)
{ {
_bufferWriter.WriteByte((byte)packet.ReasonCode); _bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter); _propertiesWriter.WriteTo(_bufferWriter);
@@ -526,7 +533,7 @@ namespace MQTTnet.Formatter.V5
static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttPacket packet) static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttPacket packet)
{ {
// SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1]. // SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1].
if (packetIdentifier == 0) if (packetIdentifier == 0)
{ {
@@ -534,4 +541,4 @@ namespace MQTTnet.Formatter.V5
} }
} }
} }
} }
@@ -22,20 +22,20 @@ namespace MQTTnet.Implementations
NetworkStream _networkStream; NetworkStream _networkStream;
public CrossPlatformSocket(AddressFamily addressFamily) public CrossPlatformSocket(AddressFamily addressFamily, ProtocolType protocolType)
{ {
_socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); _socket = new Socket(addressFamily, SocketType.Stream, protocolType);
#if !NET5_0_OR_GREATER #if !NET5_0_OR_GREATER
_socketDisposeAction = _socket.Dispose; _socketDisposeAction = _socket.Dispose;
#endif #endif
} }
public CrossPlatformSocket() public CrossPlatformSocket(ProtocolType protocolType)
{ {
// Having this constructor is important because avoiding the address family as parameter // Having this constructor is important because avoiding the address family as parameter
// will make use of dual mode in the .net framework. // will make use of dual mode in the .net framework.
_socket = new Socket(SocketType.Stream, ProtocolType.Tcp); _socket = new Socket(SocketType.Stream, protocolType);
#if !NET5_0_OR_GREATER #if !NET5_0_OR_GREATER
_socketDisposeAction = _socket.Dispose; _socketDisposeAction = _socket.Dispose;
@@ -142,12 +142,14 @@ namespace MQTTnet.Implementations
set => _socket.SendTimeout = value; set => _socket.SendTimeout = value;
} }
public async Task<CrossPlatformSocket> AcceptAsync() public async Task<CrossPlatformSocket> AcceptAsync(CancellationToken cancellationToken)
{ {
try try
{ {
#if NET452 || NET461 #if NET452 || NET461
var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false); var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false);
#elif NET7_0_OR_GREATER
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
#else #else
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false); var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
#endif #endif
@@ -170,11 +172,16 @@ namespace MQTTnet.Implementations
_socket.Bind(localEndPoint); _socket.Bind(localEndPoint);
} }
public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) public Task ConnectAsync(string host, int port, CancellationToken cancellationToken)
{ {
if (host is null) return ConnectAsync(new DnsEndPoint(host, port), cancellationToken);
}
public async Task ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
{
if (endPoint is null)
{ {
throw new ArgumentNullException(nameof(host)); throw new ArgumentNullException(nameof(endPoint));
} }
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -191,15 +198,34 @@ namespace MQTTnet.Implementations
#endif #endif
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
await _socket.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); await _socket.ConnectAsync(endPoint, cancellationToken).ConfigureAwait(false);
#else #else
// Workaround for: https://github.com/dotnet/corefx/issues/24430 // Workaround for: https://github.com/dotnet/corefx/issues/24430
using (cancellationToken.Register(_socketDisposeAction)) using (cancellationToken.Register(_socketDisposeAction))
{ {
#if NET452 || NET461 #if NET452 || NET461
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, host, port, null).ConfigureAwait(false); // This is a fix for Mono which behaves differently than dotnet.
// The connection will not be established when the DNS endpoint is used.
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
{
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, dns.Host, dns.Port, null).ConfigureAwait(false);
}
else
{
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, endPoint, null).ConfigureAwait(false);
}
#else #else
await _socket.ConnectAsync(host, port).ConfigureAwait(false);
// This is a fix for Mono which behaves differently than dotnet.
// The connection will not be established when the DNS endpoint is used.
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
{
await _socket.ConnectAsync(dns.Host, dns.Port).ConfigureAwait(false);
}
else
{
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
}
#endif #endif
} }
#endif #endif
@@ -217,7 +243,7 @@ namespace MQTTnet.Implementations
throw new MqttCommunicationTimedOutException(); throw new MqttCommunicationTimedOutException();
} }
throw new MqttCommunicationException($"Error while connecting with host '{host}:{port}'.", socketException); throw new MqttCommunicationException($"Error while connecting host '{endPoint}'.", socketException);
} }
catch (ObjectDisposedException) catch (ObjectDisposedException)
{ {
@@ -17,6 +17,7 @@ using MQTTnet.Channel;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Server; using MQTTnet.Server;
using System.Runtime.InteropServices.WindowsRuntime; using System.Runtime.InteropServices.WindowsRuntime;
using System.Net;
namespace MQTTnet.Implementations namespace MQTTnet.Implementations
{ {
@@ -65,9 +66,22 @@ namespace MQTTnet.Implementations
_socket.Control.KeepAlive = true; _socket.Control.KeepAlive = true;
} }
string hostName;
string serviceName;
if (_options.RemoteEndpoint is DnsEndPoint dns)
{
hostName = dns.Host;
serviceName = dns.Port.ToString();
}
else
{
throw new NotSupportedException("UWP only supports a DNS endpoint.");
}
if (_options.TlsOptions?.UseTls != true) if (_options.TlsOptions?.UseTls != true)
{ {
await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString()).AsTask().ConfigureAwait(false); await _socket.ConnectAsync(new HostName(hostName), serviceName).AsTask().ConfigureAwait(false);
} }
else else
{ {
@@ -88,7 +102,7 @@ namespace MQTTnet.Implementations
socketProtectionLevel = SocketProtectionLevel.Tls10; socketProtectionLevel = SocketProtectionLevel.Tls10;
} }
await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString(), socketProtectionLevel).AsTask().ConfigureAwait(false); await _socket.ConnectAsync(new HostName(hostName), serviceName, socketProtectionLevel).AsTask().ConfigureAwait(false);
} }
Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort;
@@ -5,6 +5,7 @@
#if !WINDOWS_UWP #if !WINDOWS_UWP
using System; using System;
using System.IO; using System.IO;
using System.Net;
using System.Net.Security; using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
@@ -15,6 +16,7 @@ using MQTTnet.Channel;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Protocol;
namespace MQTTnet.Implementations namespace MQTTnet.Implementations
{ {
@@ -62,11 +64,11 @@ namespace MQTTnet.Implementations
{ {
if (_tcpOptions.AddressFamily == AddressFamily.Unspecified) if (_tcpOptions.AddressFamily == AddressFamily.Unspecified)
{ {
socket = new CrossPlatformSocket(); socket = new CrossPlatformSocket(_tcpOptions.ProtocolType);
} }
else else
{ {
socket = new CrossPlatformSocket(_tcpOptions.AddressFamily); socket = new CrossPlatformSocket(_tcpOptions.AddressFamily, _tcpOptions.ProtocolType);
} }
if (_tcpOptions.LocalEndpoint != null) if (_tcpOptions.LocalEndpoint != null)
@@ -77,7 +79,12 @@ namespace MQTTnet.Implementations
socket.ReceiveBufferSize = _tcpOptions.BufferSize; socket.ReceiveBufferSize = _tcpOptions.BufferSize;
socket.SendBufferSize = _tcpOptions.BufferSize; socket.SendBufferSize = _tcpOptions.BufferSize;
socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds; socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds;
socket.NoDelay = _tcpOptions.NoDelay;
if (_tcpOptions.ProtocolType == ProtocolType.Tcp)
{
// Other protocol types do not support the Nagle algorithm.
socket.NoDelay = _tcpOptions.NoDelay;
}
if (socket.LingerState != null) if (socket.LingerState != null)
{ {
@@ -92,7 +99,30 @@ namespace MQTTnet.Implementations
socket.DualMode = _tcpOptions.DualMode.Value; socket.DualMode = _tcpOptions.DualMode.Value;
} }
await socket.ConnectAsync(_tcpOptions.Server, _tcpOptions.GetPort(), cancellationToken).ConfigureAwait(false); // This block is only for backward compatibility.
if (_tcpOptions.RemoteEndpoint == null && !string.IsNullOrEmpty(_tcpOptions.Server))
{
int port;
if (_tcpOptions.Port.HasValue)
{
port = _tcpOptions.Port.Value;
}
else
{
if (_tcpOptions.TlsOptions?.UseTls == true)
{
port = MqttPorts.Secure;
}
else
{
port = MqttPorts.Default;
}
}
_tcpOptions.RemoteEndpoint = new DnsEndPoint(_tcpOptions.Server, port, AddressFamily.Unspecified);
}
await socket.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -103,10 +133,31 @@ namespace MQTTnet.Implementations
var targetHost = _tcpOptions.TlsOptions.TargetHost; var targetHost = _tcpOptions.TlsOptions.TargetHost;
if (string.IsNullOrEmpty(targetHost)) if (string.IsNullOrEmpty(targetHost))
{ {
targetHost = _tcpOptions.Server; if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns)
{
targetHost = dns.Host;
}
}
SslStream sslStream;
if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null)
{
sslStream = new SslStream(
networkStream,
false,
InternalUserCertificateValidationCallback,
InternalUserCertificateSelectionCallback);
}
else
{
// Use a different constructor depending on the options for MQTTnet so that we do not have
// to copy the exact same behavior of the selection handler.
sslStream = new SslStream(
networkStream,
false,
InternalUserCertificateValidationCallback);
} }
var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback);
try try
{ {
#if NETCOREAPP3_1_OR_GREATER #if NETCOREAPP3_1_OR_GREATER
@@ -115,14 +166,29 @@ namespace MQTTnet.Implementations
ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols,
ClientCertificates = LoadCertificates(), ClientCertificates = LoadCertificates(),
EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol,
CertificateRevocationCheckMode = CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors
_tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode, ? X509RevocationMode.NoCheck
: _tcpOptions.TlsOptions.RevocationMode,
TargetHost = targetHost, TargetHost = targetHost,
CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy, CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy,
EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy, EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy,
AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation
}; };
#if NET7_0_OR_GREATER
if (_tcpOptions.TlsOptions.TrustChain?.Count > 0)
{
sslOptions.CertificateChainPolicy = new X509ChainPolicy
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown,
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode
};
sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain);
}
#endif
await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false);
#else #else
await sslStream.AuthenticateAsClientAsync( await sslStream.AuthenticateAsClientAsync(
@@ -272,19 +338,34 @@ namespace MQTTnet.Implementations
} }
} }
X509Certificate InternalUserCertificateSelectionCallback(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler;
if (certificateSelectionHandler != null)
{
var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions);
return certificateSelectionHandler(eventArgs);
}
if (localCertificates?.Count > 0)
{
return localCertificates[0];
}
return null;
}
bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{ {
var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null) if (certificateValidationHandler != null)
{ {
var eventArgs = new MqttClientCertificateValidationEventArgs var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
{
Certificate = x509Certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _tcpOptions
};
return certificateValidationHandler(eventArgs); return certificateValidationHandler(eventArgs);
} }
@@ -27,7 +27,7 @@ namespace MQTTnet.Implementations
readonly MqttServerOptions _serverOptions; readonly MqttServerOptions _serverOptions;
readonly MqttServerTcpEndpointBaseOptions _options; readonly MqttServerTcpEndpointBaseOptions _options;
readonly MqttServerTlsTcpEndpointOptions _tlsOptions; readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
CrossPlatformSocket _socket; CrossPlatformSocket _socket;
IPEndPoint _localEndPoint; IPEndPoint _localEndPoint;
@@ -65,7 +65,7 @@ namespace MQTTnet.Implementations
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null); _logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
_socket = new CrossPlatformSocket(_addressFamily); _socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp);
// Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2 // Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2
if (_options.ReuseAddress) if (_options.ReuseAddress)
@@ -87,33 +87,33 @@ namespace MQTTnet.Implementations
{ {
_socket.KeepAlive = _options.KeepAlive.Value; _socket.KeepAlive = _options.KeepAlive.Value;
} }
if (_options.TcpKeepAliveInterval.HasValue) if (_options.TcpKeepAliveInterval.HasValue)
{ {
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value; _socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value;
} }
if (_options.TcpKeepAliveRetryCount.HasValue) if (_options.TcpKeepAliveRetryCount.HasValue)
{ {
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value; _socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value;
} }
if (_options.TcpKeepAliveTime.HasValue) if (_options.TcpKeepAliveTime.HasValue)
{ {
_socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value; _socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value;
} }
_socket.Bind(_localEndPoint); _socket.Bind(_localEndPoint);
// Get the local endpoint back from the socket. The port may have changed. // Get the local endpoint back from the socket. The port may have changed.
// This can happen when port 0 is used. Then the OS will choose the next free port. // This can happen when port 0 is used. Then the OS will choose the next free port.
_localEndPoint = (IPEndPoint)_socket.LocalEndPoint; _localEndPoint = (IPEndPoint)_socket.LocalEndPoint;
_options.Port = _localEndPoint.Port; _options.Port = _localEndPoint.Port;
_socket.Listen(_options.ConnectionBacklog); _socket.Listen(_options.ConnectionBacklog);
_logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint); _logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint);
Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger); Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger);
return true; return true;
@@ -141,7 +141,7 @@ namespace MQTTnet.Implementations
{ {
try try
{ {
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false); var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
if (clientSocket == null) if (clientSocket == null)
{ {
continue; continue;
@@ -183,7 +183,7 @@ namespace MQTTnet.Implementations
clientSocket.NoDelay = _options.NoDelay; clientSocket.NoDelay = _options.NoDelay;
stream = clientSocket.GetStream(); stream = clientSocket.GetStream();
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate(); var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
if (clientCertificate != null) if (clientCertificate != null)
{ {
if (!clientCertificate.HasPrivateKey) if (!clientCertificate.HasPrivateKey)
@@ -228,7 +228,7 @@ namespace MQTTnet.Implementations
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger)) using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
{ {
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation; clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
@@ -109,7 +109,7 @@ namespace MQTTnet.Implementations
// A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2]. // A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2].
await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false); await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false);
#else #else
// The lock is required because the client will throw an exception if _SendAsync_ is // The lock is required because the client will throw an exception if _SendAsync_ is
// called from multiple threads at the same time. But this issue only happens with several // called from multiple threads at the same time. But this issue only happens with several
// framework versions. // framework versions.
if (_sendLock == null) if (_sendLock == null)
@@ -156,7 +156,7 @@ namespace MQTTnet.Implementations
#else #else
var proxyUri = new Uri(_options.ProxyOptions.Address); var proxyUri = new Uri(_options.ProxyOptions.Address);
WebProxy webProxy; WebProxy webProxy;
if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) 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);
@@ -164,9 +164,9 @@ namespace MQTTnet.Implementations
} }
else else
{ {
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList);
} }
if (_options.ProxyOptions.UseDefaultCredentials) if (_options.ProxyOptions.UseDefaultCredentials)
{ {
// Only update the property if required because setting it to false will alter // Only update the property if required because setting it to false will alter
@@ -234,7 +234,7 @@ namespace MQTTnet.Implementations
{ {
clientWebSocket.Options.Credentials = _options.Credentials; clientWebSocket.Options.Credentials = _options.Credentials;
} }
var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null) if (certificateValidationHandler != null)
{ {
@@ -245,15 +245,11 @@ namespace MQTTnet.Implementations
#elif WINDOWS_UWP #elif WINDOWS_UWP
throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'."); throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'.");
#elif NET452 || NET461 || NET48 #elif NET452 || NET461 || NET48
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{ {
var context = new MqttClientCertificateValidationEventArgs var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options)
{ {
Sender = sender, Sender = sender
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
}; };
return certificateValidationHandler(context); return certificateValidationHandler(context);
@@ -262,17 +258,16 @@ namespace MQTTnet.Implementations
clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{ {
// TODO: Find a way to add client options to same callback. Problem is that they have a different type. // TODO: Find a way to add client options to same callback. Problem is that they have a different type.
var context = new MqttClientCertificateValidationEventArgs var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options);
{
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
return certificateValidationHandler(context); return certificateValidationHandler(context);
}; };
#endif #endif
var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler;
if (certificateSelectionHandler != null)
{
throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections.");
}
} }
} }
} }
+7 -2
View File
@@ -104,17 +104,22 @@ namespace MQTTnet.Internal
_signal.Dispose(); _signal.Dispose();
} }
public void DropFirstItem(MqttPacketBusPartition partition) public MqttPacketBusItem DropFirstItem(MqttPacketBusPartition partition)
{ {
lock (_syncRoot) lock (_syncRoot)
{ {
var partitionInstance = _partitions[(int)partition]; var partitionInstance = _partitions[(int)partition];
if (partitionInstance.Any()) if (partitionInstance.Count > 0)
{ {
var firstItem = partitionInstance.First.Value;
partitionInstance.RemoveFirst(); partitionInstance.RemoveFirst();
return firstItem;
} }
} }
return null;
} }
public void EnqueueItem(MqttPacketBusItem item, MqttPacketBusPartition partition) public void EnqueueItem(MqttPacketBusItem item, MqttPacketBusPartition partition)
@@ -19,7 +19,6 @@ namespace MQTTnet.LowLevelClient
readonly IMqttClientAdapterFactory _clientAdapterFactory; readonly IMqttClientAdapterFactory _clientAdapterFactory;
readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>(); readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>();
readonly MqttNetSourceLogger _logger; readonly MqttNetSourceLogger _logger;
readonly IMqttNetLogger _rootLogger; readonly IMqttNetLogger _rootLogger;
IMqttChannelAdapter _adapter; IMqttChannelAdapter _adapter;
@@ -17,4 +17,5 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cmanagement/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cmanagement/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Coptions/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Coptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cstatus/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cstatus/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cstopping/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cstorage/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=server_005Cstorage/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+11
View File
@@ -11,6 +11,7 @@ using MQTTnet.Diagnostics;
using MQTTnet.Implementations; using MQTTnet.Implementations;
using MQTTnet.LowLevelClient; using MQTTnet.LowLevelClient;
using MQTTnet.Server; using MQTTnet.Server;
using MQTTnet.Server.Disconnecting;
using MqttClient = MQTTnet.Client.MqttClient; using MqttClient = MQTTnet.Client.MqttClient;
namespace MQTTnet namespace MQTTnet
@@ -175,6 +176,16 @@ namespace MQTTnet
return new MqttServer(options, serverAdapters, DefaultLogger); return new MqttServer(options, serverAdapters, DefaultLogger);
} }
public MqttServerClientDisconnectOptionsBuilder CreateMqttServerClientDisconnectOptionsBuilder()
{
return new MqttServerClientDisconnectOptionsBuilder();
}
public MqttServerStopOptionsBuilder CreateMqttServerStopOptionsBuilder()
{
return new MqttServerStopOptionsBuilder();
}
public MqttServerOptionsBuilder CreateServerOptionsBuilder() public MqttServerOptionsBuilder CreateServerOptionsBuilder()
{ {
return new MqttServerOptionsBuilder(); return new MqttServerOptionsBuilder();
+13
View File
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Protocol
{
public static class MqttPorts
{
public const int Default = 1883;
public const int Secure = 8883;
}
}
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Server.Disconnecting
{
public sealed class MqttServerClientDisconnectOptions
{
public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection;
/// <summary>
/// The reason string is sent to every client via a DISCONNECT packet.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public string ReasonString { get; set; }
/// <summary>
/// The server reference is sent to every client via a DISCONNECT packet.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public string ServerReference { get; set; }
/// <summary>
/// These user properties are sent to every client via a DISCONNECT packet.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public List<MqttUserProperty> UserProperties { get; set; }
}
}
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Server.Disconnecting
{
public sealed class MqttServerClientDisconnectOptionsBuilder
{
readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions();
public MqttServerClientDisconnectOptions Build()
{
return _options;
}
public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value)
{
_options.ReasonCode = value;
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value)
{
_options.ReasonString = value;
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value)
{
_options.ServerReference = value;
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List<MqttUserProperty> value)
{
_options.UserProperties = value;
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
}
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Server
{
public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs
{
public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped)
{
SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId));
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
IsDropped = isDropped;
}
public string SenderClientId { get; }
public string ReceiverClientId { get; }
public bool IsDropped { get; }
public MqttApplicationMessage ApplicationMessage { get; }
}
}
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Packets;
namespace MQTTnet.Server
{
public sealed class QueueMessageOverwrittenEventArgs : EventArgs
{
public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet)
{
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
}
public MqttPacket Packet { get; }
public string ReceiverClientId { get; }
}
}
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Server
{
public enum EnqueueDataPacketResult
{
Enqueued,
Dropped
}
}
+24 -17
View File
@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Adapter; using MQTTnet.Adapter;
@@ -14,6 +15,7 @@ using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
@@ -95,15 +97,16 @@ namespace MQTTnet.Server
{ {
var cancellationToken = _cancellationToken.Token; var cancellationToken = _cancellationToken.Token;
IsRunning = true; IsRunning = true;
_ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false); _ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default)
.ConfigureAwait(false);
await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false); await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false);
} }
finally finally
{ {
IsRunning = false; IsRunning = false;
Session.DisconnectedTimestamp = DateTime.UtcNow; Session.DisconnectedTimestamp = DateTime.UtcNow;
_cancellationToken?.TryCancel(); _cancellationToken?.TryCancel();
@@ -141,7 +144,7 @@ namespace MQTTnet.Server
Statistics.HandleSentPacket(packet); Statistics.HandleSentPacket(packet);
} }
public async Task StopAsync(MqttDisconnectReasonCode reason) public async Task StopAsync(MqttServerClientDisconnectOptions disconnectOptions)
{ {
IsRunning = false; IsRunning = false;
@@ -150,16 +153,20 @@ namespace MQTTnet.Server
// Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+. // Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+.
if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500) if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500)
{ {
// The Client or Server MAY send a DISCONNECT packet before closing the Network Connection. // From RFC: The Client or Server MAY send a DISCONNECT packet before closing the Network Connection.
// This library does not sent a DISCONNECT packet for a normal disconnection. Maybe adding // This library does not sent a DISCONNECT packet for a normal disconnection.
// a configuration option is requested in the future. // TODO: Maybe adding a configuration option is requested in the future.
if (reason != MqttDisconnectReasonCode.NormalDisconnection) if (disconnectOptions != null)
{ {
// Is is very important to send the DISCONNECT packet here BEFORE cancelling the if (disconnectOptions.ReasonCode != MqttDisconnectReasonCode.NormalDisconnection || disconnectOptions.UserProperties?.Any() == true ||
// token because the entire connection is closed (disposed) as soon as the cancellation !string.IsNullOrEmpty(disconnectOptions.ReasonString) || !string.IsNullOrEmpty(disconnectOptions.ServerReference))
// token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive {
// at the client! // Is is very important to send the DISCONNECT packet here BEFORE cancelling the
await TrySendDisconnectPacket(reason).ConfigureAwait(false); // token because the entire connection is closed (disposed) as soon as the cancellation
// token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive
// at the client!
await TrySendDisconnectPacket(disconnectOptions).ConfigureAwait(false);
}
} }
} }
} }
@@ -219,7 +226,7 @@ namespace MQTTnet.Server
if (dispatchApplicationMessageResult.CloseConnection) if (dispatchApplicationMessageResult.CloseConnection)
{ {
await StopAsync(MqttDisconnectReasonCode.UnspecifiedError); await StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.UnspecifiedError });
return; return;
} }
@@ -550,14 +557,14 @@ namespace MQTTnet.Server
_cancellationToken?.TryCancel(); _cancellationToken?.TryCancel();
} }
async Task TrySendDisconnectPacket(MqttDisconnectReasonCode reasonCode) async Task TrySendDisconnectPacket(MqttServerClientDisconnectOptions options)
{ {
try try
{ {
// This also indicates that it was tried at least! // This also indicates that it was tried at least!
_disconnectPacketSent = true; _disconnectPacketSent = true;
var disconnectPacket = MqttPacketFactories.Disconnect.Create(reasonCode); var disconnectPacket = MqttPacketFactories.Disconnect.Create(options);
using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout)) using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout))
{ {
@@ -566,7 +573,7 @@ namespace MQTTnet.Server
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Warning(exception, "Client '{0}': Error while sending DISCONNECT packet (ReasonCode = {1})", Id, reasonCode); _logger.Warning(exception, "Client '{0}': Error while sending DISCONNECT packet (ReasonCode = {1})", Id, options.ReasonCode);
} }
} }
} }
@@ -16,6 +16,7 @@ using MQTTnet.Formatter;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
@@ -32,11 +33,11 @@ namespace MQTTnet.Server
readonly MqttRetainedMessagesManager _retainedMessagesManager; readonly MqttRetainedMessagesManager _retainedMessagesManager;
readonly IMqttNetLogger _rootLogger; readonly IMqttNetLogger _rootLogger;
readonly ReaderWriterLockSlim _sessionsManagementLock = new ReaderWriterLockSlim();
// The _sessions dictionary contains all session, the _subscriberSessions hash set contains subscriber sessions only. // The _sessions dictionary contains all session, the _subscriberSessions hash set contains subscriber sessions only.
// See the MqttSubscription object for a detailed explanation. // See the MqttSubscription object for a detailed explanation.
readonly MqttSessionsStorage _sessionsStorage = new MqttSessionsStorage(); readonly MqttSessionsStorage _sessionsStorage = new MqttSessionsStorage();
readonly ReaderWriterLockSlim _sessionsManagementLock = new ReaderWriterLockSlim();
readonly HashSet<MqttSession> _subscriberSessions = new HashSet<MqttSession>(); readonly HashSet<MqttSession> _subscriberSessions = new HashSet<MqttSession>();
public MqttClientSessionsManager( public MqttClientSessionsManager(
@@ -58,8 +59,13 @@ namespace MQTTnet.Server
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
} }
public async Task CloseAllConnections() public async Task CloseAllConnections(MqttServerClientDisconnectOptions options)
{ {
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
List<MqttClient> connections; List<MqttClient> connections;
lock (_clients) lock (_clients)
{ {
@@ -69,7 +75,7 @@ namespace MQTTnet.Server
foreach (var connection in connections) foreach (var connection in connections)
{ {
await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); await connection.StopAsync(options).ConfigureAwait(false);
} }
} }
@@ -101,7 +107,7 @@ namespace MQTTnet.Server
{ {
if (connection != null) if (connection != null)
{ {
await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); await connection.StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.NormalDisconnection }).ConfigureAwait(false);
} }
} }
catch (Exception exception) catch (Exception exception)
@@ -204,7 +210,7 @@ namespace MQTTnet.Server
{ {
continue; continue;
} }
if (_eventContainer.InterceptingClientEnqueueEvent.HasHandlers) if (_eventContainer.InterceptingClientEnqueueEvent.HasHandlers)
{ {
var eventArgs = new InterceptingClientApplicationMessageEnqueueEventArgs(senderId, session.Id, applicationMessage); var eventArgs = new InterceptingClientApplicationMessageEnqueueEventArgs(senderId, session.Id, applicationMessage);
@@ -238,13 +244,25 @@ namespace MQTTnet.Server
matchingSubscribersCount++; matchingSubscribersCount++;
session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketCopy)); var result = session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketCopy));
if (_eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.HasHandlers)
{
var eventArgs = new ApplicationMessageEnqueuedEventArgs(senderId, session.Id, applicationMessage, result == EnqueueDataPacketResult.Dropped);
await _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
}
_logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'", session.Id, applicationMessage.Topic); _logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'", session.Id, applicationMessage.Topic);
} }
if (matchingSubscribersCount == 0) if (matchingSubscribersCount == 0)
{ {
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers; if (reasonCode == (int)MqttPubAckReasonCode.Success)
{
// Only change the value if it was success. Otherwise, we would hide an error or not authorized status.
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
}
await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false); await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false);
} }
} }
@@ -366,7 +384,8 @@ namespace MQTTnet.Server
if (_eventContainer.ClientConnectedEvent.HasHandlers) if (_eventContainer.ClientConnectedEvent.HasHandlers)
{ {
var eventArgs = new ClientConnectedEventArgs(connectPacket, var eventArgs = new ClientConnectedEventArgs(
connectPacket,
channelAdapter.PacketFormatterAdapter.ProtocolVersion, channelAdapter.PacketFormatterAdapter.ProtocolVersion,
channelAdapter.Endpoint, channelAdapter.Endpoint,
client.Session.Items); client.Session.Items);
@@ -595,13 +614,13 @@ namespace MQTTnet.Server
session.IsPersistent = sessionShouldPersist; session.IsPersistent = sessionShouldPersist;
session.DisconnectedTimestamp = null; session.DisconnectedTimestamp = null;
session.Recover(); session.Recover();
connAckPacket.IsSessionPresent = true; connAckPacket.IsSessionPresent = true;
} }
} }
_sessionsStorage.UpdateSession(connectPacket.ClientId, session); _sessionsStorage.UpdateSession(connectPacket.ClientId, session);
// Create a new client (always required). // Create a new client (always required).
lock (_clients) lock (_clients)
{ {
@@ -631,7 +650,8 @@ namespace MQTTnet.Server
if (oldClient != null) if (oldClient != null)
{ {
await oldClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false); // TODO: Consider event here for session takeover to allow manipulation of user properties etc.
await oldClient.StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.SessionTakenOver }).ConfigureAwait(false);
if (_eventContainer.ClientDisconnectedEvent.HasHandlers) if (_eventContainer.ClientDisconnectedEvent.HasHandlers)
{ {
@@ -734,4 +754,4 @@ namespace MQTTnet.Server
return eventArgs; return eventArgs;
} }
} }
} }
@@ -92,7 +92,7 @@ namespace MQTTnet.Server
} }
} }
private struct Statistics struct Statistics
{ {
public long _receivedPacketsCount; public long _receivedPacketsCount;
public long _sentPacketsCount; public long _sentPacketsCount;
@@ -22,6 +22,10 @@ namespace MQTTnet.Server
public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>(); public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>();
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>(); public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
public AsyncEvent<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>(); public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
@@ -8,11 +8,20 @@ using System.Threading.Tasks;
using MQTTnet.Diagnostics; using MQTTnet.Diagnostics;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
public sealed class MqttServerKeepAliveMonitor public sealed class MqttServerKeepAliveMonitor
{ {
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions
{
ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout,
UserProperties = null,
ReasonString = null,
ServerReference = null
};
readonly MqttNetSourceLogger _logger; readonly MqttNetSourceLogger _logger;
readonly MqttServerOptions _options; readonly MqttServerOptions _options;
readonly MqttClientSessionsManager _sessionsManager; readonly MqttClientSessionsManager _sessionsManager;
@@ -43,12 +52,12 @@ namespace MQTTnet.Server
{ {
try try
{ {
_logger.Info("Starting keep alive monitor."); _logger.Info("Starting keep alive monitor");
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
TryProcessClients(); TryProcessClients();
Sleep(_options.KeepAliveMonitorInterval); Sleep(_options.KeepAliveOptions.MonitorInterval);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -56,11 +65,11 @@ namespace MQTTnet.Server
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts."); _logger.Error(exception, "Unhandled exception while checking keep alive timeouts");
} }
finally finally
{ {
_logger.Verbose("Stopped checking keep alive timeout."); _logger.Verbose("Stopped checking keep alive timeout");
} }
} }
@@ -98,13 +107,6 @@ namespace MQTTnet.Server
return; return;
} }
if (connection.ChannelAdapter.IsReadingPacket)
{
// The connection is currently reading a (large) packet. So it is obviously
// doing something and thus "connected".
return;
}
// Values described here: [MQTT-3.1.2-24]. // Values described here: [MQTT-3.1.2-24].
// If the client sends 5 sec. the server will allow up to 7.5 seconds. // If the client sends 5 sec. the server will allow up to 7.5 seconds.
// If the client sends 1 sec. the server will allow up to 1.5 seconds. // If the client sends 1 sec. the server will allow up to 1.5 seconds.
@@ -117,18 +119,18 @@ namespace MQTTnet.Server
return; return;
} }
_logger.Warning("Client '{0}': Did not receive any packet or keep alive signal.", connection.Id); _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id);
// Execute the disconnection in background so that the keep alive monitor can continue // Execute the disconnection in background so that the keep alive monitor can continue
// with checking other connections. // with checking other connections.
// We do not need to wait for the task so no await is needed. // We do not need to wait for the task so no await is needed.
// Also the internal state of the connection must be swapped to "Finalizing" because the // Also the internal state of the connection must be swapped to "Finalizing" because the
// next iteration of the keep alive timer happens. // next iteration of the keep alive timer happens.
var _ = connection.StopAsync(MqttDisconnectReasonCode.KeepAliveTimeout); _ = connection.StopAsync(DefaultDisconnectOptions);
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.Id); _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id);
} }
} }
+11 -3
View File
@@ -18,6 +18,7 @@ namespace MQTTnet.Server
public sealed class MqttSession : IDisposable public sealed class MqttSession : IDisposable
{ {
readonly MqttClientSessionsManager _clientSessionsManager; readonly MqttClientSessionsManager _clientSessionsManager;
readonly MqttServerEventContainer _eventContainer;
readonly MqttPacketBus _packetBus = new MqttPacketBus(); readonly MqttPacketBus _packetBus = new MqttPacketBus();
readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider();
@@ -44,6 +45,7 @@ namespace MQTTnet.Server
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
_clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager)); _clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager));
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
_subscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager); _subscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager);
} }
@@ -117,20 +119,25 @@ namespace MQTTnet.Server
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Control); _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Control);
} }
public void EnqueueDataPacket(MqttPacketBusItem packetBusItem) public EnqueueDataPacketResult EnqueueDataPacket(MqttPacketBusItem packetBusItem)
{ {
if (_packetBus.ItemsCount(MqttPacketBusPartition.Data) >= _serverOptions.MaxPendingMessagesPerClient) if (_packetBus.ItemsCount(MqttPacketBusPartition.Data) >= _serverOptions.MaxPendingMessagesPerClient)
{ {
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage)
{ {
return; return EnqueueDataPacketResult.Dropped;
} }
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)
{ {
// Only drop from the data partition. Dropping from control partition might break the connection // Only drop from the data partition. Dropping from control partition might break the connection
// because the client does not receive PINGREQ packets etc. any longer. // because the client does not receive PINGREQ packets etc. any longer.
_packetBus.DropFirstItem(MqttPacketBusPartition.Data); var firstItem = _packetBus.DropFirstItem(MqttPacketBusPartition.Data);
if (firstItem != null && _eventContainer.QueuedApplicationMessageOverwrittenEvent.HasHandlers)
{
var eventArgs = new QueueMessageOverwrittenEventArgs(Id, firstItem.Packet);
_eventContainer.QueuedApplicationMessageOverwrittenEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
}
} }
} }
@@ -147,6 +154,7 @@ namespace MQTTnet.Server
} }
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data); _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data);
return EnqueueDataPacketResult.Enqueued;
} }
public void EnqueueHealthPacket(MqttPacketBusItem packetBusItem) public void EnqueueHealthPacket(MqttPacketBusItem packetBusItem)
+56 -16
View File
@@ -14,6 +14,7 @@ using MQTTnet.Diagnostics;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
@@ -29,6 +30,7 @@ namespace MQTTnet.Server
readonly IMqttNetLogger _rootLogger; readonly IMqttNetLogger _rootLogger;
CancellationTokenSource _cancellationTokenSource; CancellationTokenSource _cancellationTokenSource;
bool _isStopping;
public MqttServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) public MqttServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger)
{ {
@@ -49,6 +51,12 @@ namespace MQTTnet.Server
_keepAliveMonitor = new MqttServerKeepAliveMonitor(options, _clientSessionsManager, _rootLogger); _keepAliveMonitor = new MqttServerKeepAliveMonitor(options, _clientSessionsManager, _rootLogger);
} }
public event Func<ApplicationMessageEnqueuedEventArgs, Task> ApplicationMessageEnqueuedOrDroppedAsync
{
add => _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.AddHandler(value);
remove => _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.RemoveHandler(value);
}
public event Func<ApplicationMessageNotConsumedEventArgs, Task> ApplicationMessageNotConsumedAsync public event Func<ApplicationMessageNotConsumedEventArgs, Task> ApplicationMessageNotConsumedAsync
{ {
add => _eventContainer.ApplicationMessageNotConsumedEvent.AddHandler(value); add => _eventContainer.ApplicationMessageNotConsumedEvent.AddHandler(value);
@@ -133,6 +141,12 @@ namespace MQTTnet.Server
remove => _eventContainer.PreparingSessionEvent.RemoveHandler(value); remove => _eventContainer.PreparingSessionEvent.RemoveHandler(value);
} }
public event Func<QueueMessageOverwrittenEventArgs, Task> QueuedApplicationMessageOverwrittenAsync
{
add => _eventContainer.QueuedApplicationMessageOverwrittenEvent.AddHandler(value);
remove => _eventContainer.QueuedApplicationMessageOverwrittenEvent.RemoveHandler(value);
}
public event Func<RetainedMessageChangedEventArgs, Task> RetainedMessageChangedAsync public event Func<RetainedMessageChangedEventArgs, Task> RetainedMessageChangedAsync
{ {
add => _eventContainer.RetainedMessageChangedEvent.AddHandler(value); add => _eventContainer.RetainedMessageChangedEvent.AddHandler(value);
@@ -169,6 +183,13 @@ namespace MQTTnet.Server
remove => _eventContainer.ValidatingConnectionEvent.RemoveHandler(value); remove => _eventContainer.ValidatingConnectionEvent.RemoveHandler(value);
} }
/// <summary>
/// Gets or sets whether the server will accept new connections.
/// If not, the server will close the connection without any notification (DISCONNECT packet).
/// This feature can be used when the server is shutting down.
/// </summary>
public bool AcceptNewConnections { get; set; } = true;
public bool IsStarted => _cancellationTokenSource != null; public bool IsStarted => _cancellationTokenSource != null;
/// <summary> /// <summary>
@@ -184,16 +205,21 @@ namespace MQTTnet.Server
return _retainedMessagesManager?.ClearMessages() ?? CompletedTask.Instance; return _retainedMessagesManager?.ClearMessages() ?? CompletedTask.Instance;
} }
public Task DisconnectClientAsync(string id, MqttDisconnectReasonCode reasonCode) public Task DisconnectClientAsync(string id, MqttServerClientDisconnectOptions options)
{ {
if (id == null) if (id == null)
{ {
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
ThrowIfNotStarted(); ThrowIfNotStarted();
return _clientSessionsManager.GetClient(id).StopAsync(reasonCode); return _clientSessionsManager.GetClient(id).StopAsync(options);
} }
public Task<IList<MqttClientStatus>> GetClientsAsync() public Task<IList<MqttClientStatus>> GetClientsAsync()
@@ -203,13 +229,6 @@ namespace MQTTnet.Server
return _clientSessionsManager.GetClientsStatus(); return _clientSessionsManager.GetClientsStatus();
} }
public Task<IList<MqttApplicationMessage>> GetRetainedMessagesAsync()
{
ThrowIfNotStarted();
return _retainedMessagesManager.GetMessages();
}
public Task<MqttApplicationMessage> GetRetainedMessageAsync(string topic) public Task<MqttApplicationMessage> GetRetainedMessageAsync(string topic)
{ {
if (topic == null) if (topic == null)
@@ -222,6 +241,13 @@ namespace MQTTnet.Server
return _retainedMessagesManager.GetMessage(topic); return _retainedMessagesManager.GetMessage(topic);
} }
public Task<IList<MqttApplicationMessage>> GetRetainedMessagesAsync()
{
ThrowIfNotStarted();
return _retainedMessagesManager.GetMessages();
}
public Task<IList<MqttSessionStatus>> GetSessionsAsync() public Task<IList<MqttSessionStatus>> GetSessionsAsync()
{ {
ThrowIfNotStarted(); ThrowIfNotStarted();
@@ -247,7 +273,7 @@ namespace MQTTnet.Server
if (string.IsNullOrEmpty(injectedApplicationMessage.ApplicationMessage.Topic)) if (string.IsNullOrEmpty(injectedApplicationMessage.ApplicationMessage.Topic))
{ {
throw new NotSupportedException("Injected application messages must contain a topic. Topic alias is not supported."); throw new NotSupportedException("Injected application messages must contain a topic (topic alias is not supported)");
} }
var sessionItems = injectedApplicationMessage.CustomSessionItems ?? ServerSessionItems; var sessionItems = injectedApplicationMessage.CustomSessionItems ?? ServerSessionItems;
@@ -263,6 +289,8 @@ namespace MQTTnet.Server
{ {
ThrowIfStarted(); ThrowIfStarted();
_isStopping = false;
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token; var cancellationToken = _cancellationTokenSource.Token;
@@ -278,11 +306,16 @@ namespace MQTTnet.Server
await _eventContainer.StartedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); await _eventContainer.StartedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
_logger.Info("Started."); _logger.Info("Started");
} }
public async Task StopAsync() public async Task StopAsync(MqttServerStopOptions options)
{ {
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
try try
{ {
if (_cancellationTokenSource == null) if (_cancellationTokenSource == null)
@@ -290,9 +323,11 @@ namespace MQTTnet.Server
return; return;
} }
_isStopping = true;
_cancellationTokenSource.Cancel(false); _cancellationTokenSource.Cancel(false);
await _clientSessionsManager.CloseAllConnections().ConfigureAwait(false); await _clientSessionsManager.CloseAllConnections(options.DefaultClientDisconnectOptions).ConfigureAwait(false);
foreach (var adapter in _adapters) foreach (var adapter in _adapters)
{ {
@@ -308,7 +343,7 @@ namespace MQTTnet.Server
await _eventContainer.StoppedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); await _eventContainer.StoppedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
_logger.Info("Stopped."); _logger.Info("Stopped");
} }
public Task SubscribeAsync(string clientId, ICollection<MqttTopicFilter> topicFilters) public Task SubscribeAsync(string clientId, ICollection<MqttTopicFilter> topicFilters)
@@ -362,14 +397,14 @@ namespace MQTTnet.Server
ThrowIfDisposed(); ThrowIfDisposed();
ThrowIfNotStarted(); ThrowIfNotStarted();
return _retainedMessagesManager?.UpdateMessage(null, retainedMessage); return _retainedMessagesManager?.UpdateMessage(string.Empty, retainedMessage);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{ {
StopAsync().GetAwaiter().GetResult(); StopAsync(new MqttServerStopOptions()).GetAwaiter().GetResult();
foreach (var adapter in _adapters) foreach (var adapter in _adapters)
{ {
@@ -382,6 +417,11 @@ namespace MQTTnet.Server
Task OnHandleClient(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) Task OnHandleClient(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken)
{ {
if (_isStopping || !AcceptNewConnections)
{
return CompletedTask.Instance;
}
return _clientSessionsManager.HandleClientConnectionAsync(channelAdapter, cancellationToken); return _clientSessionsManager.HandleClientConnectionAsync(channelAdapter, cancellationToken);
} }
@@ -8,11 +8,22 @@ using System.Threading.Tasks;
using MQTTnet.Internal; using MQTTnet.Internal;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
public static class MqttServerExtensions public static class MqttServerExtensions
{ {
public static Task DisconnectClientAsync(this MqttServer server, string id, MqttDisconnectReasonCode reasonCode = MqttDisconnectReasonCode.NormalDisconnection)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
return server.DisconnectClientAsync(id, new MqttServerClientDisconnectOptions { ReasonCode = reasonCode });
}
public static Task InjectApplicationMessage( public static Task InjectApplicationMessage(
this MqttServer server, this MqttServer server,
string topic, string topic,
@@ -47,6 +58,16 @@ namespace MQTTnet.Server
})); }));
} }
public static Task StopAsync(this MqttServer server)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
return server.StopAsync(new MqttServerStopOptions());
}
public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters) public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters)
{ {
if (server == null) if (server == null)
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Server
{
public sealed class MqttServerKeepAliveOptions
{
/// <summary>
/// When this mode is enabled the MQTT server will not close a connection when the
/// client is currently sending a (large) payload. This may lead to "dead" connections
/// When this mode is disabled the MQTT server will disconnect a client when the keep
/// alive timeout is reached even if the client is currently sending a (large) payload.
/// </summary>
public bool DisconnectClientWhenReadingPayload { get; set; }
public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500);
}
}
@@ -9,12 +9,19 @@ namespace MQTTnet.Server
public sealed class MqttServerOptions public sealed class MqttServerOptions
{ {
public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(100); public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(100);
public MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } = new MqttServerTcpEndpointOptions(); public MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } = new MqttServerTcpEndpointOptions();
public bool EnablePersistentSessions { get; set; } public bool EnablePersistentSessions { get; set; }
public TimeSpan KeepAliveMonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500); [Obsolete("Use KeepAliveOptions instead.")]
public TimeSpan KeepAliveMonitorInterval
{
get => KeepAliveOptions.MonitorInterval;
set => KeepAliveOptions.MonitorInterval = value;
}
public MqttServerKeepAliveOptions KeepAliveOptions { get; } = new MqttServerKeepAliveOptions();
public int MaxPendingMessagesPerClient { get; set; } = 250; public int MaxPendingMessagesPerClient { get; set; } = 250;
@@ -5,7 +5,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using MQTTnet.Formatter; using MQTTnet.Formatter;
using MQTTnet.Protocol; using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
@@ -18,41 +18,46 @@ namespace MQTTnet.Server
_client = client ?? throw new ArgumentNullException(nameof(client)); _client = client ?? throw new ArgumentNullException(nameof(client));
} }
/// <summary> public long BytesReceived => _client.ChannelAdapter.BytesReceived;
/// Gets or sets the client identifier.
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. public long BytesSent => _client.ChannelAdapter.BytesSent;
/// </summary>
public string Id => _client.Id; public DateTime ConnectedTimestamp => _client.Statistics.ConnectedTimestamp;
public string Endpoint => _client.Endpoint; public string Endpoint => _client.Endpoint;
public MqttProtocolVersion ProtocolVersion => _client.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; /// <summary>
/// Gets or sets the client identifier.
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
/// </summary>
public string Id => _client.Id;
public DateTime ConnectedTimestamp => _client.Statistics.ConnectedTimestamp; public DateTime LastNonKeepAlivePacketReceivedTimestamp => _client.Statistics.LastNonKeepAlivePacketReceivedTimestamp;
public DateTime LastPacketReceivedTimestamp => _client.Statistics.LastPacketReceivedTimestamp; public DateTime LastPacketReceivedTimestamp => _client.Statistics.LastPacketReceivedTimestamp;
public DateTime LastPacketSentTimestamp => _client.Statistics.LastPacketSentTimestamp; public DateTime LastPacketSentTimestamp => _client.Statistics.LastPacketSentTimestamp;
public DateTime LastNonKeepAlivePacketReceivedTimestamp => _client.Statistics.LastNonKeepAlivePacketReceivedTimestamp; public MqttProtocolVersion ProtocolVersion => _client.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion;
public long ReceivedApplicationMessagesCount => _client.Statistics.ReceivedApplicationMessagesCount; public long ReceivedApplicationMessagesCount => _client.Statistics.ReceivedApplicationMessagesCount;
public long SentApplicationMessagesCount => _client.Statistics.SentApplicationMessagesCount;
public long ReceivedPacketsCount => _client.Statistics.ReceivedPacketsCount; public long ReceivedPacketsCount => _client.Statistics.ReceivedPacketsCount;
public long SentApplicationMessagesCount => _client.Statistics.SentApplicationMessagesCount;
public long SentPacketsCount => _client.Statistics.SentPacketsCount; public long SentPacketsCount => _client.Statistics.SentPacketsCount;
public MqttSessionStatus Session { get; set; } public MqttSessionStatus Session { get; set; }
public long BytesSent => _client.ChannelAdapter.BytesSent; public Task DisconnectAsync(MqttServerClientDisconnectOptions options)
public long BytesReceived => _client.ChannelAdapter.BytesReceived;
public Task DisconnectAsync()
{ {
return _client.StopAsync(MqttDisconnectReasonCode.NormalDisconnection); if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
return _client.StopAsync(options);
} }
public void ResetStatistics() public void ResetStatistics()
@@ -60,4 +65,4 @@ namespace MQTTnet.Server
_client.ResetStatistics(); _client.ResetStatistics();
} }
} }
} }
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server
{
public static class MqttClientStatusExtensions
{
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions
{
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection,
ReasonString = null,
UserProperties = null,
ServerReference = null
};
public static Task DisconnectAsync(this MqttClientStatus clientStatus)
{
if (clientStatus == null)
{
throw new ArgumentNullException(nameof(clientStatus));
}
return clientStatus.DisconnectAsync(DefaultDisconnectOptions);
}
}
}
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Protocol;
using MQTTnet.Server.Disconnecting;
namespace MQTTnet.Server
{
public sealed class MqttServerStopOptions
{
/// <summary>
/// These disconnect options are sent to every connected client via a DISCONNECT packet.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions
{
ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown,
UserProperties = null,
ReasonString = null,
ServerReference = null
};
}
}

Some files were not shown because too many files have changed in this diff Show More