Compare commits

..
Author SHA1 Message Date
Jeff 798a0e2250 Validate MQTT 5 SUBSCRIBE identifiers (#2262) 2026-09-30 20:55:47 +02:00
Jeff 6e5351ce75 Reject duplicate MQTT 5 PUBLISH singleton properties (#2261) 2026-09-30 20:25:24 +02:00
Jochen Kühner 483447e783 Fix unobserved task exceptions when pending messages overflow (#2272)
Observe packet task failures to prevent unobserved overflow exceptions

Server dispatch and session enqueue operations create packet bus items without
waiting for delivery. When the pending-message queue overflows, Fail faults
the item's completion task, which can later raise UnobservedTaskException
even if the application handles the dropped-message notification.

Observe the completion task's exception inside MqttPacketBusItem.Fail. This
keeps the task faulted and preserves the original exception for existing and
future WaitAsync callers, including DeliverApplicationMessageAsync.

Add a garbage-collection regression test for failures without a waiter and
verify that requesting the task before or after failure preserves the original
overflow exception. Re-enable unobserved-exception tracking in the existing
overflow integration tests for both drop strategies.

Before the fix, the focused run failed the new unobserved-exception test and
both enqueue overflow tests. After the fix, all nine focused cases and all
489 tests in the .NET 10 Release test suite pass.

Fixes https://github.com/dotnet/MQTTnet/issues/2271
2026-09-30 20:23:57 +02:00
Jan Klass 18731d9f02 Drop unintended rendered space in README (#2258)
The line break lead to github rendering a space in `( broker`.
2026-08-09 15:20:52 +02:00
Suhas Holla Karkada Chandrashekar 7be55ce922 Fix default TLS certificate validation (BREAKING CHANGE) (#2254)
Fix default TLS certificate validation
2026-07-03 17:56:41 +02:00
christian 14463d1ef9 Merge remote-tracking branch 'origin/master' 2026-06-21 16:24:00 +02:00
christian eeb0f7a261 Update release notes 2026-06-21 16:23:56 +02:00
Suhas Holla Karkada Chandrashekar 52c8f00ef8 Fix keep alive ping send timeout disconnect handling (#2253)
Fix keep alive ping send timeout disconnect
2026-06-21 16:19:45 +02:00
christian bef2ec6fba Update version 2026-06-20 11:02:42 +02:00
Christian d8f06280d5 Migrate to SLNX (#2252) 2026-06-20 11:00:27 +02:00
christian cf68e5e8d5 Update release notes 2026-06-18 17:55:07 +02:00
Alexander KöpkeandCopilot 68da92fd4b Add support for pluggable stream provider to enable SOCKS5 proxies (#2251)
* Add SOCKS5 proxy support via pluggable stream provider

Introduces MQTTnet.Extensions.Socks5, enabling SOCKS5 proxy support for MQTTnet TCP transport. Adds IMqttClientStreamProvider interface, Socks5StreamProvider implementation, and related configuration options. Updates MqttTcpChannel to support stream providers and TLS layering. Includes unit tests with a fake SOCKS5 server and usage samples. Solution and project references updated accordingly.

* Add SOCKS5 options validation for invalid configurations

* Tighten SOCKS5 RFC compliance checks

---------

Co-authored-by: Copilot <Copilot@users.noreply.github.com>
2026-06-18 17:51:52 +02:00
c1b46b0ae7 Client: Tolerate late acknowledgement packets (#2078, #2079) (#2247)
When a publish, subscribe or unsubscribe request was cancelled or
timed out client-side, the corresponding awaitable was removed from
the packet dispatcher. A subsequent PUBACK / PUBCOMP / SUBACK /
UNSUBACK arriving from the broker then fell through to the default
case and raised an MqttProtocolViolationException, which forced a
disconnect. Under high publish load (issue #2079) and from multiple
threads (issue #2078) this manifested as sporadic connection drops.

Match these acknowledgement packets explicitly and log a warning
when no awaiter is registered, instead of treating it as a protocol
violation. The connection is now preserved.

Also documents the thread-safety guarantees of IMqttClient: publish,
subscribe, unsubscribe and ping operations are safe to invoke
concurrently. Connect / disconnect must still be coordinated.

Co-authored-by: meisa <meisa@MAFDEL>
Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
2026-06-16 20:28:40 +02:00
christian 745d1d25fe Update release notes 2026-06-13 18:07:57 +02:00
christian 905f070c37 Reset release notes 2026-06-13 18:06:14 +02:00
Suhas Holla Karkada Chandrashekar bca2ded8eb Fix MQTT v5 topic alias forwarding (#2250) 2026-06-13 18:04:42 +02:00
christian 8321806acf Fix nuget package generation 2026-01-25 11:50:48 +01:00
christian 081ef3077a Trigger actions 2026-01-22 18:18:13 +01:00
christian a3fbb928d6 Improve CI 2025-12-21 16:46:53 +01:00
christian 42ba46b885 Update release notes 2025-12-20 10:26:19 +01:00
Alexander Köpke 07409e3a35 Allow user properties as pre-encoded UTF-8 binary buffer (#2228)
* Add support for binary user properties in MQTT 5.0

* Optimize byte array reversal in SubscriptionTopicHashTests

* Use InvariantCulture for topic string conversion in Load_Tests

* switched to `Microsoft.Testing.Platform`

* Refactor CI workflow to separate signed and unsigned package builds

* Add condition to sign job to prevent execution on external pull requests

* Remove fallback to value as string in MqttUserProperty

* Refactor MqttUserProperty handling to use byte arrays for improved performance and add deprecation warnings for legacy methods
2025-12-18 17:46:35 +01:00
Christian 1a3e97afec Add support for dotnet 10 (#2229)
* Add dotnet10 as target framework and fix breaking changes

* Fix tests

* Fix tests for CI

* Fix typo
2025-12-07 16:24:34 +01:00
88 changed files with 3767 additions and 400 deletions
+15 -13
View File
@@ -3,7 +3,7 @@ name: CI
on: [push, pull_request]
env:
VERSION: "5.1.0.${{github.run_number}}"
VERSION: "5.2.0.${{github.run_number}}"
PACKAGE_SUFFIX: ""
jobs:
@@ -17,19 +17,26 @@ jobs:
with:
dotnet-version: |
8.0.x
10.0.x
- name: Checkout code
uses: actions/checkout@v4
- name: Load strong name certificate
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: |
echo "$SNC_BASE64" | base64 --decode > "${{ github.workspace }}/certificate.snk"
shell: bash
env:
SNC_BASE64: ${{ secrets.SNC_BASE64 }}
- name: Build package
run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}/certificate.snk
- name: Build package (signed)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: dotnet build MQTTnet.slnx --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}/certificate.snk
- name: Build package (unsigned)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
run: dotnet build MQTTnet.slnx --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }}
- name: Upload nuget packages
uses: actions/upload-artifact@v4
@@ -48,24 +55,19 @@ jobs:
with:
dotnet-version: |
8.0.x
10.0.x
- name: Checkout code
uses: actions/checkout@v4
- name: Execute tests
run: dotnet test --framework net8.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj
run: dotnet test --framework net10.0 --configuration Release --project Source/MQTTnet.Tests/MQTTnet.Tests.csproj
sign:
needs: build
runs-on: windows-latest # Code signing must run on a Windows agent for Authenticode signing (dll/exe)
if: github.repository == 'dotnet/MQTTnet'
if: github.repository == 'dotnet/MQTTnet' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
- name: Download nuget packages
uses: actions/download-artifact@v4
with:
@@ -73,7 +75,7 @@ jobs:
path: nugets
- name: Install sign CLI tool
run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3
run: dotnet tool install --tool-path . sign --version 0.9.1-beta.25379.1
- name: Sign nugets
shell: pwsh
@@ -114,7 +116,7 @@ jobs:
run: dotnet nuget push ${{ github.workspace }}/nugets/**/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
publish-nuget:
# Tag schema is 'v5.0.0-rc' etc.
# Tag schema is 'v5.1.0-rc' etc.
if: startsWith(github.ref, 'refs/tags/v')
needs: [sign, test]
runs-on: ubuntu-latest
+3 -27
View File
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Product>MQTTnet</Product>
<Authors>The contributors of MQTTnet</Authors>
<Company>The contributors of MQTTnet</Company>
@@ -11,6 +11,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<NeutralLanguage>en-US</NeutralLanguage>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -24,35 +25,10 @@
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>recommended</AnalysisMode>
<AnalysisModeStyle>default</AnalysisModeStyle>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<EnableMSTestRunner>true</EnableMSTestRunner>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<PropertyGroup Condition="'$(GITHUB_ACTIONS)'=='true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(GeneratePackageOnBuild)'=='true'">
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>nuget.png</PackageIcon>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Condition="'$(GeneratePackageOnBuild)'=='true'">
<None Include="README.md" Pack="true" PackagePath="" Condition="Exists('README.md')" />
<None Include="..\..\Images\nuget.png" Pack="true" PackagePath="" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeSymbols)'=='true'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>
</Project>
+24
View File
@@ -0,0 +1,24 @@
<Project>
<PropertyGroup Condition="'$(GeneratePackageOnBuild)'=='true'">
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>nuget.png</PackageIcon>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Condition="'$(GeneratePackageOnBuild)'=='true'">
<None Include="README.md" Pack="true" PackagePath="" Condition="Exists('README.md')" />
<None Include="..\..\Images\nuget.png" Pack="true" PackagePath="" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeSymbols)'=='true'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>
</Project>
-89
View File
@@ -1,89 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31919.166
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\MQTTnet.csproj", "{3587E506-55A2-4EB3-99C7-DC01E42D25D2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
ProjectSection(SolutionItems) = preProject
.github\workflows\ci.yml = .github\workflows\ci.yml
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
Directory.Build.props = Directory.Build.props
LICENSE = LICENSE
README.md = README.md
Source\ReleaseNotes.md = Source\ReleaseNotes.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.Rpc", "Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj", "{C444E9C8-95FA-430E-9126-274129DE16CD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}"
EndProject
Project("{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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Server", "Source\MQTTnet.Server\MQTTnet.Server.csproj", "{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3587E506-55A2-4EB3-99C7-DC01E42D25D2}.Release|Any CPU.Build.0 = Release|Any CPU
{F10C4060-F7EE-4A83-919F-FF723E72F94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F10C4060-F7EE-4A83-919F-FF723E72F94A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F10C4060-F7EE-4A83-919F-FF723E72F94A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F10C4060-F7EE-4A83-919F-FF723E72F94A}.Release|Any CPU.Build.0 = Release|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C444E9C8-95FA-430E-9126-274129DE16CD}.Release|Any CPU.Build.0 = Release|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71CF35F5-3327-4A91-AAF4-5340F6701771}.Release|Any CPU.Build.0 = Release|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B270F32A-9F3E-42EE-A989-813E35E29ADB}.Release|Any CPU.Build.0 = Release|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}.Release|Any CPU.Build.0 = Release|Any CPU
{175D5340-CC5B-4542-939D-4E7D15A0BC8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{175D5340-CC5B-4542-939D-4E7D15A0BC8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{175D5340-CC5B-4542-939D-4E7D15A0BC8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{175D5340-CC5B-4542-939D-4E7D15A0BC8D}.Release|Any CPU.Build.0 = Release|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.ActiveCfg = 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.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
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
EndGlobalSection
EndGlobal
+28
View File
@@ -0,0 +1,28 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Solution Items/">
<File Path=".github/workflows/ci.yml" />
<File Path="CODE-OF-CONDUCT.md" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="LICENSE" />
<File Path="README.md" />
<File Path="Source/ReleaseNotes.md" />
</Folder>
<Project Path="Samples/MQTTnet.Samples.csproj" />
<Project Path="Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj" />
<Project Path="Source/MQTTnet.AspTestApp/MQTTnet.AspTestApp.csproj" />
<Project Path="Source/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj" />
<Project Path="Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj" />
<Project Path="Source/MQTTnet.Extensions.Socks5/MQTTnet.Extensions.Socks5.csproj" />
<Project Path="Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj" />
<Project Path="Source/MQTTnet.Server/MQTTnet.Server.csproj" />
<Project Path="Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj" />
<Project Path="Source/MQTTnet.Tests.Extensions.Socks5/MQTTnet.Tests.Extensions.Socks5.csproj" />
<Project Path="Source/MQTTnet.Tests/MQTTnet.Tests.csproj" />
<Project Path="Source/MQTTnet/MQTTnet.csproj" />
</Solution>
+3 -3
View File
@@ -12,9 +12,9 @@
# MQTTnet
MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (
broker) and supports the MQTT protocol up to version 5. It is compatible with mostly any supported .NET Framework
version and CPU architecture.
MQTTnet is a high performance .NET library for MQTT based communication.
It provides a MQTT client and a MQTT server (broker) and supports the MQTT protocol up to version 5.
It is compatible with mostly any supported .NET Framework version and CPU architecture.
## Features
+20
View File
@@ -6,6 +6,9 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using System;
using System.Text;
namespace MQTTnet.Samples.Client;
public static class Client_Publish_Samples
@@ -84,4 +87,21 @@ public static class Client_Publish_Samples
Console.WriteLine("MQTT application message is published.");
}
public static MqttApplicationMessage Create_Message_With_Binary_User_Property()
{
/*
* MQTT v5 user properties are encoded as UTF-8 strings. When the UTF-8 payload is already available
* as a byte buffer, the builder APIs can avoid creating intermediate strings.
*/
var encodedValue = Encoding.UTF8.GetBytes("sensor-01");
return new MqttApplicationMessageBuilder()
.WithTopic("samples/metadata/binary")
.WithUserProperty("client-id", encodedValue.AsMemory())
.WithUserProperty("checksum", new ArraySegment<byte>(encodedValue))
.WithPayload("metadata")
.Build();
}
}
+86
View File
@@ -0,0 +1,86 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Extensions.Socks5;
using MQTTnet.Samples.Helpers;
namespace MQTTnet.Samples.Client;
public static class Client_Socks5_Samples
{
public static async Task Connect_Tcp_Via_Socks5_No_Auth()
{
/*
* This sample connects to an MQTT broker through a SOCKS5 proxy that does not
* require authentication. The SOCKS5 stream provider performs the proxy handshake
* (RFC 1928) and returns a connected stream that the MQTT TCP channel can use as if
* it were a direct TCP connection.
*
* Host name resolution is performed by the proxy by default (ATYP=DOMAINNAME), which
* is the desired behavior when the MQTT broker hostname is only reachable from inside
* the network the proxy lives in.
*/
var mqttFactory = new MqttClientFactory();
using var mqttClient = mqttFactory.CreateMqttClient();
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("broker.internal.example", 1883)
.WithSocks5Proxy(socks => socks
.WithHost("proxy.example.com")
.WithPort(1080))
.Build();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var response = await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token);
Console.WriteLine("The MQTT client is connected through SOCKS5.");
response.DumpToConsole();
var disconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder().Build();
await mqttClient.DisconnectAsync(disconnectOptions, CancellationToken.None);
}
public static async Task Connect_Tcp_With_Tls_Via_Socks5_With_Credentials()
{
/*
* This sample connects to a TLS-protected MQTT broker through a SOCKS5 proxy that
* requires username/password authentication (RFC 1929).
*
* Note that the TLS handshake is layered on top of the SOCKS5 tunnel: the SNI used
* by SslStream is derived from the broker hostname (or the value passed to
* WithTargetHost), not from the proxy hostname.
*/
var mqttFactory = new MqttClientFactory();
using var mqttClient = mqttFactory.CreateMqttClient();
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("broker.internal.example", 8883)
.WithTlsOptions(tls => tls
.WithTargetHost("broker.internal.example"))
.WithSocks5Proxy(socks => socks
.WithHost("proxy.example.com")
.WithPort(1080)
.WithCredentials("alice", "s3cr3t")
.WithResolveDnsRemotely(true)
.WithHandshakeTimeout(TimeSpan.FromSeconds(10)))
.Build();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var response = await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token);
Console.WriteLine("The MQTT client is connected through SOCKS5 with TLS.");
response.DumpToConsole();
var disconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder().Build();
await mqttClient.DisconnectAsync(disconnectOptions, CancellationToken.None);
}
}
+2 -1
View File
@@ -7,6 +7,7 @@
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
using System.Text;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Samples.Helpers;
@@ -67,7 +68,7 @@ public static class Client_Subscribe_Samples
args.ResponseReasonString = "That did not work!";
// User properties require MQTT v5!
args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data"));
args.ResponseUserProperties.Add(new MqttUserProperty("My", Encoding.UTF8.GetBytes("Data")));
// Now the broker will resend the message again.
return Task.CompletedTask;
+1
View File
@@ -12,6 +12,7 @@
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj"/>
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj"/>
<ProjectReference Include="..\Source\MQTTnet.Extensions.Socks5\MQTTnet.Extensions.Socks5.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Server\MQTTnet.Server.csproj"/>
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj"/>
+2
View File
@@ -59,10 +59,12 @@ public static class Server_TLS_Samples
certRequest.CertificateExtensions.Add(sanBuilder.Build());
using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10));
#pragma warning disable SYSLIB0057
var pfxCertificate = new X509Certificate2(
certificate.Export(X509ContentType.Pfx),
(string)null!,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
#pragma warning restore SYSLIB0057
return pfxCertificate;
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>MQTTnet.Extensions.Socks5</AssemblyName>
<RootNamespace>MQTTnet.Extensions.Socks5</RootNamespace>
<IsAotCompatible>true</IsAotCompatible>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Description>This is an extension library which adds SOCKS5 proxy support (RFC 1928 / RFC 1929) to the MQTTnet TCP transport.</Description>
<PackageId>MQTTnet.Extensions.Socks5</PackageId>
<PackageTags>MQTT Message Queue Telemetry Transport MQTTClient SOCKS SOCKS5 Proxy Tunnel Networking IoT InternetOfThings</PackageTags>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
</ItemGroup>
</Project>
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// Sugar extensions for configuring a SOCKS5 proxy on the MQTT client options builder.
/// </summary>
public static class MqttClientOptionsBuilderExtensions
{
/// <summary>
/// Configures the client to tunnel its TCP connection to the broker through a SOCKS5
/// proxy. A TCP transport (via <c>WithTcpServer(...)</c> or <c>WithEndPoint(...)</c>)
/// must be configured first.
/// </summary>
public static MqttClientOptionsBuilder WithSocks5Proxy(this MqttClientOptionsBuilder builder, Socks5ProxyOptions options)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(options);
return builder.WithStreamProvider(new Socks5StreamProvider(options));
}
/// <summary>
/// Configures the client to tunnel its TCP connection to the broker through a SOCKS5
/// proxy using the fluent <see cref="Socks5ProxyOptionsBuilder"/>.
/// </summary>
public static MqttClientOptionsBuilder WithSocks5Proxy(this MqttClientOptionsBuilder builder, Action<Socks5ProxyOptionsBuilder> configure)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);
var optionsBuilder = new Socks5ProxyOptionsBuilder();
configure(optionsBuilder);
return WithSocks5Proxy(builder, optionsBuilder.Build());
}
}
@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Net;
using System.Net.Sockets;
using MQTTnet.Channel;
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// Wraps a <see cref="NetworkStream"/> opened by the SOCKS5 stream provider so
/// <c>MqttTcpChannel</c> can read the original socket's endpoints via
/// <see cref="IMqttClientStreamEndPoints"/> while the rest of the I/O delegates to the
/// underlying stream.
/// </summary>
sealed class Socks5NetworkStream : Stream, IMqttClientStreamEndPoints
{
readonly NetworkStream _inner;
public Socks5NetworkStream(NetworkStream inner, EndPoint localEndPoint, EndPoint remoteEndPoint)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
LocalEndPoint = localEndPoint;
RemoteEndPoint = remoteEndPoint;
}
public EndPoint LocalEndPoint { get; }
public EndPoint RemoteEndPoint { get; }
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
public override bool CanTimeout => _inner.CanTimeout;
public override long Length => _inner.Length;
public override long Position
{
get => _inner.Position;
set => _inner.Position = value;
}
public override int ReadTimeout
{
get => _inner.ReadTimeout;
set => _inner.ReadTimeout = value;
}
public override int WriteTimeout
{
get => _inner.WriteTimeout;
set => _inner.WriteTimeout = value;
}
public override void Flush() => _inner.Flush();
public override Task FlushAsync(CancellationToken cancellationToken) => _inner.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override int Read(Span<byte> buffer) => _inner.Read(buffer);
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _inner.ReadAsync(buffer, offset, count, cancellationToken);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _inner.ReadAsync(buffer, cancellationToken);
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void SetLength(long value) => _inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
public override void Write(ReadOnlySpan<byte> buffer) => _inner.Write(buffer);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _inner.WriteAsync(buffer, offset, count, cancellationToken);
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> _inner.WriteAsync(buffer, cancellationToken);
public override void Close()
{
_inner.Close();
base.Close();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_inner.Dispose();
}
base.Dispose(disposing);
}
public override async ValueTask DisposeAsync()
{
await _inner.DisposeAsync().ConfigureAwait(false);
await base.DisposeAsync().ConfigureAwait(false);
}
}
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// Protocol constants for SOCKS5 (RFC 1928) and the username/password sub-negotiation
/// (RFC 1929). Internal use only.
/// </summary>
internal static class Socks5Protocol
{
public const byte Version = 0x05;
public const byte AuthSubNegotiationVersion = 0x01;
public const byte MethodNoAuth = 0x00;
public const byte MethodGssApi = 0x01;
public const byte MethodUsernamePassword = 0x02;
public const byte MethodNoneAcceptable = 0xFF;
public const byte CommandConnect = 0x01;
public const byte Reserved = 0x00;
public const byte AddressTypeIPv4 = 0x01;
public const byte AddressTypeDomainName = 0x03;
public const byte AddressTypeIPv6 = 0x04;
public const byte ReplySucceeded = 0x00;
public const byte ReplyGeneralFailure = 0x01;
public const byte ReplyConnectionNotAllowed = 0x02;
public const byte ReplyNetworkUnreachable = 0x03;
public const byte ReplyHostUnreachable = 0x04;
public const byte ReplyConnectionRefused = 0x05;
public const byte ReplyTtlExpired = 0x06;
public const byte ReplyCommandNotSupported = 0x07;
public const byte ReplyAddressTypeNotSupported = 0x08;
public const int MaxStringLength = 255;
}
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Net.Sockets;
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// Options for the SOCKS5 stream provider (RFC 1928 / RFC 1929).
/// A new provider can be constructed from these options via
/// <see cref="MqttClientOptionsBuilderExtensions.WithSocks5Proxy(MqttClientOptionsBuilder, Socks5ProxyOptions)"/>.
/// </summary>
public sealed class Socks5ProxyOptions
{
/// <summary>
/// The SOCKS5 proxy host name or IP literal. Must be set.
/// </summary>
public string Host { get; set; }
/// <summary>
/// The SOCKS5 proxy port. Defaults to 1080.
/// </summary>
public int Port { get; set; } = 1080;
/// <summary>
/// The user name used for the username/password authentication sub-negotiation (RFC 1929).
/// When <c>null</c> or empty, the client will only offer the "no authentication" method.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The password (raw bytes per RFC 1929) used for the username/password authentication
/// sub-negotiation. Must be 1..255 bytes when <see cref="Username"/> is set.
/// </summary>
public byte[] Password { get; set; }
/// <summary>
/// When <c>true</c> (the default) the broker hostname is forwarded to the proxy as a
/// SOCKS5 <c>DOMAINNAME</c> address type, so DNS resolution happens at the proxy
/// (typical when the broker is behind the proxy in a private network).
/// When <c>false</c>, the hostname is resolved locally before sending the CONNECT request
/// and an IPv4 / IPv6 address type is used.
/// </summary>
public bool ResolveDnsRemotely { get; set; } = true;
/// <summary>
/// The maximum time allowed for the full SOCKS5 handshake (TCP connect to proxy + greeting
/// + optional auth + CONNECT). The handshake will be cancelled and a
/// <see cref="MQTTnet.Exceptions.MqttProxyException"/> will be thrown when this elapses.
/// Defaults to 30 seconds.
/// </summary>
public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Address family preference for the socket connecting to the proxy and for local DNS
/// resolution when <see cref="ResolveDnsRemotely"/> is <c>false</c>.
/// </summary>
public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified;
public override string ToString()
{
return $"{Host}:{Port}";
}
}
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Net.Sockets;
using System.Text;
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// Fluent builder for <see cref="Socks5ProxyOptions"/>.
/// </summary>
public sealed class Socks5ProxyOptionsBuilder
{
readonly Socks5ProxyOptions _options = new();
public Socks5ProxyOptions Build()
{
if (string.IsNullOrEmpty(_options.Host))
{
throw new InvalidOperationException("A SOCKS5 proxy host must be configured via WithHost.");
}
if (_options.Port <= 0 || _options.Port > 65535)
{
throw new InvalidOperationException("The SOCKS5 proxy port must be in the range 1..65535.");
}
if (_options.HandshakeTimeout <= TimeSpan.Zero)
{
throw new InvalidOperationException("The SOCKS5 handshake timeout must be greater than zero.");
}
if (string.IsNullOrEmpty(_options.Username) && _options.Password is { Length: > 0 })
{
throw new InvalidOperationException("A SOCKS5 password cannot be configured without a username.");
}
if (!string.IsNullOrEmpty(_options.Username) && (_options.Password == null || _options.Password.Length == 0))
{
throw new InvalidOperationException("A SOCKS5 password must be configured when a username is set.");
}
return _options;
}
public Socks5ProxyOptionsBuilder WithHost(string host)
{
_options.Host = host ?? throw new ArgumentNullException(nameof(host));
return this;
}
public Socks5ProxyOptionsBuilder WithPort(int port)
{
_options.Port = port;
return this;
}
public Socks5ProxyOptionsBuilder WithCredentials(string username, string password)
{
_options.Username = username;
_options.Password = password != null ? Encoding.UTF8.GetBytes(password) : null;
return this;
}
public Socks5ProxyOptionsBuilder WithCredentials(string username, byte[] password)
{
_options.Username = username;
_options.Password = password;
return this;
}
/// <summary>
/// Controls whether the broker hostname is forwarded to the proxy (remote DNS, default) or
/// resolved locally before issuing the CONNECT request.
/// </summary>
public Socks5ProxyOptionsBuilder WithResolveDnsRemotely(bool resolveDnsRemotely = true)
{
_options.ResolveDnsRemotely = resolveDnsRemotely;
return this;
}
public Socks5ProxyOptionsBuilder WithHandshakeTimeout(TimeSpan timeout)
{
_options.HandshakeTimeout = timeout;
return this;
}
public Socks5ProxyOptionsBuilder WithAddressFamily(AddressFamily addressFamily)
{
_options.AddressFamily = addressFamily;
return this;
}
}
@@ -0,0 +1,507 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using MQTTnet.Channel;
using MQTTnet.Exceptions;
namespace MQTTnet.Extensions.Socks5;
/// <summary>
/// <see cref="IMqttClientStreamProvider"/> implementation that tunnels the connection through
/// a SOCKS5 proxy (RFC 1928) with optional username/password authentication (RFC 1929).
/// </summary>
public sealed class Socks5StreamProvider : IMqttClientStreamProvider
{
readonly Socks5ProxyOptions _options;
public Socks5StreamProvider(Socks5ProxyOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrEmpty(_options.Host))
{
throw new ArgumentException("Socks5ProxyOptions.Host must be set.", nameof(options));
}
if (_options.Port <= 0 || _options.Port > 65535)
{
throw new ArgumentOutOfRangeException(nameof(options), _options.Port, "Socks5ProxyOptions.Port must be in the range 1..65535.");
}
if (_options.HandshakeTimeout <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(options), _options.HandshakeTimeout, "Socks5ProxyOptions.HandshakeTimeout must be greater than zero.");
}
if (string.IsNullOrEmpty(_options.Username) && _options.Password is { Length: > 0 })
{
throw new ArgumentException("Socks5ProxyOptions.Password cannot be set without Socks5ProxyOptions.Username.", nameof(options));
}
if (!string.IsNullOrEmpty(_options.Username) && (_options.Password == null || _options.Password.Length == 0))
{
throw new ArgumentException("Socks5ProxyOptions.Password must be set when Socks5ProxyOptions.Username is set.", nameof(options));
}
}
public async Task<Stream> ConnectAsync(EndPoint brokerEndPoint, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(brokerEndPoint);
using var timeoutCts = new CancellationTokenSource(_options.HandshakeTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var handshakeToken = linkedCts.Token;
var addressFamily = _options.AddressFamily;
Socket socket = addressFamily == AddressFamily.Unspecified
? new Socket(SocketType.Stream, ProtocolType.Tcp)
: new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
NetworkStream networkStream = null;
try
{
try
{
await socket.ConnectAsync(_options.Host, _options.Port, handshakeToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyUnreachable,
$"Timed out connecting to SOCKS5 proxy after {_options.HandshakeTimeout}.",
_options.ToString());
}
catch (Exception ex) when (ex is SocketException or IOException)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyUnreachable,
$"Could not connect to SOCKS5 proxy: {ex.Message}",
_options.ToString(),
ex);
}
networkStream = new NetworkStream(socket, ownsSocket: true);
try
{
await PerformGreetingAndAuthAsync(networkStream, handshakeToken).ConfigureAwait(false);
await PerformConnectAsync(networkStream, brokerEndPoint, handshakeToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Timed out during SOCKS5 handshake after {_options.HandshakeTimeout}.",
_options.ToString());
}
catch (EndOfStreamException ex)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
"The SOCKS5 proxy closed the connection during the handshake.",
_options.ToString(),
ex);
}
catch (Exception ex) when (ex is IOException or SocketException)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"I/O error during SOCKS5 handshake: {ex.Message}",
_options.ToString(),
ex);
}
// Hand off the stream — ownership transfers to the caller.
var result = new Socks5NetworkStream(networkStream, socket.LocalEndPoint, socket.RemoteEndPoint);
networkStream = null;
socket = null;
return result;
}
finally
{
networkStream?.Dispose();
socket?.Dispose();
}
}
async Task PerformGreetingAndAuthAsync(NetworkStream stream, CancellationToken cancellationToken)
{
var hasCredentials = !string.IsNullOrEmpty(_options.Username);
// VER NMETHODS METHOD[1..n]
// We always offer no-auth; we additionally offer user/pass when credentials are configured.
byte[] greeting;
if (hasCredentials)
{
greeting = [Socks5Protocol.Version, 0x02, Socks5Protocol.MethodNoAuth, Socks5Protocol.MethodUsernamePassword];
}
else
{
greeting = [Socks5Protocol.Version, 0x01, Socks5Protocol.MethodNoAuth];
}
await stream.WriteAsync(greeting, cancellationToken).ConfigureAwait(false);
// VER METHOD
var greetingResponse = new byte[2];
await ReadExactlyAsync(stream, greetingResponse, cancellationToken).ConfigureAwait(false);
if (greetingResponse[0] != Socks5Protocol.Version)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Unexpected SOCKS version in greeting response: 0x{greetingResponse[0]:X2}.",
_options.ToString());
}
var chosenMethod = greetingResponse[1];
switch (chosenMethod)
{
case Socks5Protocol.MethodNoAuth:
if (hasCredentials)
{
// Server accepted no-auth even though we offered credentials: fine, continue.
}
return;
case Socks5Protocol.MethodUsernamePassword:
if (!hasCredentials)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
"The SOCKS5 server selected username/password authentication but no credentials are configured.",
_options.ToString());
}
await PerformUsernamePasswordAuthAsync(stream, cancellationToken).ConfigureAwait(false);
return;
case Socks5Protocol.MethodNoneAcceptable:
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthMethodRejected,
"The SOCKS5 server rejected all offered authentication methods.",
_options.ToString());
default:
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"The SOCKS5 server selected an unsupported authentication method: 0x{chosenMethod:X2}.",
_options.ToString());
}
}
async Task PerformUsernamePasswordAuthAsync(NetworkStream stream, CancellationToken cancellationToken)
{
var usernameBytes = Encoding.UTF8.GetBytes(_options.Username ?? string.Empty);
var passwordBytes = _options.Password ?? [];
if (usernameBytes.Length > Socks5Protocol.MaxStringLength)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthFailed,
$"Username exceeds {Socks5Protocol.MaxStringLength} bytes (RFC 1929).",
_options.ToString());
}
if (usernameBytes.Length == 0)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthFailed,
"Username must be 1..255 bytes (RFC 1929).",
_options.ToString());
}
if (passwordBytes.Length > Socks5Protocol.MaxStringLength)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthFailed,
$"Password exceeds {Socks5Protocol.MaxStringLength} bytes (RFC 1929).",
_options.ToString());
}
if (passwordBytes.Length == 0)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthFailed,
"Password must be 1..255 bytes (RFC 1929).",
_options.ToString());
}
var request = new byte[3 + usernameBytes.Length + passwordBytes.Length];
var offset = 0;
request[offset++] = Socks5Protocol.AuthSubNegotiationVersion;
request[offset++] = (byte)usernameBytes.Length;
Buffer.BlockCopy(usernameBytes, 0, request, offset, usernameBytes.Length);
offset += usernameBytes.Length;
request[offset++] = (byte)passwordBytes.Length;
Buffer.BlockCopy(passwordBytes, 0, request, offset, passwordBytes.Length);
await stream.WriteAsync(request, cancellationToken).ConfigureAwait(false);
var response = new byte[2];
await ReadExactlyAsync(stream, response, cancellationToken).ConfigureAwait(false);
if (response[0] != Socks5Protocol.AuthSubNegotiationVersion)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Unexpected SOCKS5 auth sub-negotiation version: 0x{response[0]:X2}.",
_options.ToString());
}
if (response[1] != 0x00)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAuthFailed,
$"The SOCKS5 server rejected the supplied credentials (status 0x{response[1]:X2}).",
_options.ToString());
}
}
async Task PerformConnectAsync(NetworkStream stream, EndPoint brokerEndPoint, CancellationToken cancellationToken)
{
var request = BuildConnectRequest(brokerEndPoint, out var targetDescription);
await stream.WriteAsync(request, cancellationToken).ConfigureAwait(false);
// Reply: VER REP RSV ATYP BND.ADDR BND.PORT
var header = new byte[4];
await ReadExactlyAsync(stream, header, cancellationToken).ConfigureAwait(false);
if (header[0] != Socks5Protocol.Version)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Unexpected SOCKS version in CONNECT response: 0x{header[0]:X2}.",
_options.ToString());
}
if (header[2] != Socks5Protocol.Reserved)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Unexpected SOCKS5 reserved byte in CONNECT response: 0x{header[2]:X2}.",
_options.ToString());
}
var rep = header[1];
if (rep != Socks5Protocol.ReplySucceeded)
{
var (code, description) = MapReplyCode(rep);
throw new MqttProxyException(
code,
$"SOCKS5 CONNECT to '{targetDescription}' failed: {description}.",
_options.ToString());
}
// Discard BND.ADDR / BND.PORT. The length depends on ATYP and we tolerate unexpected lengths.
var atyp = header[3];
switch (atyp)
{
case Socks5Protocol.AddressTypeIPv4:
await SkipAsync(stream, 4 + 2, cancellationToken).ConfigureAwait(false);
break;
case Socks5Protocol.AddressTypeIPv6:
await SkipAsync(stream, 16 + 2, cancellationToken).ConfigureAwait(false);
break;
case Socks5Protocol.AddressTypeDomainName:
var lenBuf = new byte[1];
await ReadExactlyAsync(stream, lenBuf, cancellationToken).ConfigureAwait(false);
await SkipAsync(stream, lenBuf[0] + 2, cancellationToken).ConfigureAwait(false);
break;
default:
throw new MqttProxyException(
MqttProxyErrorCode.ProxyProtocolError,
$"Unexpected SOCKS5 address type in CONNECT response: 0x{atyp:X2}.",
_options.ToString());
}
}
byte[] BuildConnectRequest(EndPoint brokerEndPoint, out string targetDescription)
{
// Layout: VER CMD RSV ATYP DST.ADDR DST.PORT
if (brokerEndPoint is DnsEndPoint dns)
{
if (_options.ResolveDnsRemotely)
{
return BuildDomainConnectRequest(dns.Host, dns.Port, out targetDescription);
}
var addresses = ResolveDns(dns.Host, dns.Port);
return BuildIpConnectRequest(addresses, dns.Port, out targetDescription);
}
if (brokerEndPoint is IPEndPoint ip)
{
return BuildIpConnectRequest([ip.Address], ip.Port, out targetDescription);
}
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAddressTypeNotSupported,
$"Broker endpoint of type '{brokerEndPoint.GetType().Name}' is not supported by the SOCKS5 stream provider.",
_options.ToString());
}
IPAddress[] ResolveDns(string host, int port)
{
try
{
var addresses = _options.AddressFamily == AddressFamily.Unspecified
? Dns.GetHostAddresses(host)
: Dns.GetHostAddresses(host, _options.AddressFamily);
if (addresses == null || addresses.Length == 0)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyHostUnreachable,
$"DNS resolution for '{host}:{port}' returned no addresses.",
_options.ToString());
}
return addresses;
}
catch (SocketException ex)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyHostUnreachable,
$"Local DNS resolution failed for '{host}:{port}': {ex.Message}",
_options.ToString(),
ex);
}
}
byte[] BuildIpConnectRequest(IPAddress[] addresses, int port, out string targetDescription)
{
var preferredFamily = _options.AddressFamily == AddressFamily.Unspecified
? AddressFamily.InterNetwork
: _options.AddressFamily;
IPAddress chosen = null;
foreach (var address in addresses)
{
if (address.AddressFamily == preferredFamily)
{
chosen = address;
break;
}
}
chosen ??= addresses[0];
targetDescription = $"{chosen}:{port}";
if (chosen.AddressFamily == AddressFamily.InterNetwork)
{
var addr = chosen.GetAddressBytes();
var request = new byte[4 + 4 + 2];
request[0] = Socks5Protocol.Version;
request[1] = Socks5Protocol.CommandConnect;
request[2] = Socks5Protocol.Reserved;
request[3] = Socks5Protocol.AddressTypeIPv4;
Buffer.BlockCopy(addr, 0, request, 4, 4);
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(8, 2), (ushort)port);
return request;
}
if (chosen.AddressFamily == AddressFamily.InterNetworkV6)
{
var addr = chosen.GetAddressBytes();
var request = new byte[4 + 16 + 2];
request[0] = Socks5Protocol.Version;
request[1] = Socks5Protocol.CommandConnect;
request[2] = Socks5Protocol.Reserved;
request[3] = Socks5Protocol.AddressTypeIPv6;
Buffer.BlockCopy(addr, 0, request, 4, 16);
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(20, 2), (ushort)port);
return request;
}
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAddressTypeNotSupported,
$"IP address family '{chosen.AddressFamily}' is not supported by SOCKS5.",
_options.ToString());
}
byte[] BuildDomainConnectRequest(string host, int port, out string targetDescription)
{
var hostBytes = Encoding.UTF8.GetBytes(host ?? string.Empty);
if (hostBytes.Length == 0)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyHostUnreachable,
"Broker host is empty.",
_options.ToString());
}
if (hostBytes.Length > Socks5Protocol.MaxStringLength)
{
throw new MqttProxyException(
MqttProxyErrorCode.ProxyAddressTypeNotSupported,
$"Broker host exceeds the SOCKS5 DOMAINNAME limit of {Socks5Protocol.MaxStringLength} bytes.",
_options.ToString());
}
targetDescription = $"{host}:{port}";
var request = new byte[4 + 1 + hostBytes.Length + 2];
request[0] = Socks5Protocol.Version;
request[1] = Socks5Protocol.CommandConnect;
request[2] = Socks5Protocol.Reserved;
request[3] = Socks5Protocol.AddressTypeDomainName;
request[4] = (byte)hostBytes.Length;
Buffer.BlockCopy(hostBytes, 0, request, 5, hostBytes.Length);
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(5 + hostBytes.Length, 2), (ushort)port);
return request;
}
static (MqttProxyErrorCode Code, string Description) MapReplyCode(byte rep)
{
return rep switch
{
Socks5Protocol.ReplyGeneralFailure => (MqttProxyErrorCode.ProxyGeneralFailure, "general SOCKS server failure"),
Socks5Protocol.ReplyConnectionNotAllowed => (MqttProxyErrorCode.ProxyConnectionNotAllowed, "connection not allowed by ruleset"),
Socks5Protocol.ReplyNetworkUnreachable => (MqttProxyErrorCode.ProxyNetworkUnreachable, "network unreachable"),
Socks5Protocol.ReplyHostUnreachable => (MqttProxyErrorCode.ProxyHostUnreachable, "host unreachable"),
Socks5Protocol.ReplyConnectionRefused => (MqttProxyErrorCode.ProxyConnectionRefused, "connection refused"),
Socks5Protocol.ReplyTtlExpired => (MqttProxyErrorCode.ProxyTtlExpired, "TTL expired"),
Socks5Protocol.ReplyCommandNotSupported => (MqttProxyErrorCode.ProxyCommandNotSupported, "command not supported"),
Socks5Protocol.ReplyAddressTypeNotSupported => (MqttProxyErrorCode.ProxyAddressTypeNotSupported, "address type not supported"),
_ => (MqttProxyErrorCode.ProxyUnknownError, $"unknown reply code 0x{rep:X2}")
};
}
static async Task ReadExactlyAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken)
{
var offset = 0;
while (offset < buffer.Length)
{
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
throw new EndOfStreamException();
}
offset += read;
}
}
static async Task SkipAsync(NetworkStream stream, int count, CancellationToken cancellationToken)
{
if (count <= 0)
{
return;
}
var buffer = new byte[count];
await ReadExactlyAsync(stream, buffer, cancellationToken).ConfigureAwait(false);
}
}
@@ -40,6 +40,7 @@ public sealed class MqttServerClientDisconnectOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
{
if (_options.UserProperties == null)
@@ -50,4 +51,26 @@ public sealed class MqttServerClientDisconnectOptionsBuilder
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -51,6 +51,7 @@ public sealed class ExchangeEnhancedAuthenticationOptionsFactory
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, string value)
{
ArgumentNullException.ThrowIfNull(name);
@@ -64,4 +65,32 @@ public sealed class ExchangeEnhancedAuthenticationOptionsFactory
return this;
}
public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
ArgumentNullException.ThrowIfNull(name);
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, ArraySegment<byte> value)
{
ArgumentNullException.ThrowIfNull(name);
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -202,9 +202,14 @@ public sealed class MqttTcpServerListener : IDisposable
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
// TODO: Check why this export is needed. Is there something else in the RemoteCertificate as a X509Certificate2???
if (clientCertificate == null && sslStream.RemoteCertificate != null)
{
#if NET10_0_OR_GREATER
clientCertificate = X509CertificateLoader.LoadCertificate(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
#else
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
#endif
}
}
@@ -218,6 +218,8 @@ public sealed class MqttConnectedClient : IDisposable
HandleTopicAlias(publishPacket);
var applicationMessage = MqttApplicationMessageFactory.Create(publishPacket);
// Topic aliases are scoped to this network connection and must not be forwarded to other clients.
applicationMessage.TopicAlias = 0;
var dispatchApplicationMessageResult =
await _sessionsManager.DispatchApplicationMessage(Id, UserName, Session.Items, applicationMessage, cancellationToken).ConfigureAwait(false);
@@ -579,4 +581,4 @@ public sealed class MqttConnectedClient : IDisposable
_logger.Warning(exception, "Client '{0}': Error while sending DISCONNECT packet (ReasonCode = {1})", Id, options.ReasonCode);
}
}
}
}
@@ -2,7 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<DebugType>Full</DebugType>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -0,0 +1,5 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
[assembly: DoNotParallelize]
@@ -0,0 +1,529 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MQTTnet.Tests.Extensions.Socks5;
/// <summary>
/// Scriptable in-process SOCKS5 server used by the test suite.
/// Listens on 127.0.0.1 on a random port and runs a single, configurable handshake per
/// accepted connection. On a successful CONNECT, it opens a TCP connection to the destination
/// described by the CONNECT request and bidirectionally relays bytes between the client and
/// the destination, allowing tests to point the SOCKS5 client at the in-memory MQTTnet
/// broker.
/// </summary>
internal sealed class FakeSocks5Server : IAsyncDisposable
{
readonly TcpListener _listener;
readonly CancellationTokenSource _cts = new();
readonly Task _acceptLoop;
readonly List<Task> _connectionTasks = new();
readonly object _connectionsLock = new();
readonly List<RecordedConnect> _recordedConnects = new();
readonly object _recordedLock = new();
public FakeSocks5Server(FakeSocks5ServerOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
_acceptLoop = Task.Run(AcceptLoopAsync);
}
public int Port { get; }
public FakeSocks5ServerOptions Options { get; }
public IReadOnlyList<RecordedConnect> RecordedConnects
{
get
{
lock (_recordedLock)
{
return _recordedConnects.ToArray();
}
}
}
public async ValueTask DisposeAsync()
{
try
{
_cts.Cancel();
}
catch (ObjectDisposedException)
{
}
try
{
_listener.Stop();
}
catch (Exception)
{
// ignore
}
try
{
await _acceptLoop.ConfigureAwait(false);
}
catch (Exception)
{
// ignore
}
Task[] pending;
lock (_connectionsLock)
{
pending = _connectionTasks.ToArray();
}
try
{
await Task.WhenAll(pending).ConfigureAwait(false);
}
catch (Exception)
{
// ignore
}
_cts.Dispose();
}
async Task AcceptLoopAsync()
{
try
{
while (!_cts.IsCancellationRequested)
{
TcpClient client;
try
{
client = await _listener.AcceptTcpClientAsync(_cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
catch (SocketException)
{
return;
}
catch (ObjectDisposedException)
{
return;
}
var task = Task.Run(() => HandleClientAsync(client));
lock (_connectionsLock)
{
_connectionTasks.Add(task);
}
}
}
catch (Exception)
{
// swallow — diagnostic-only
}
}
async Task HandleClientAsync(TcpClient client)
{
using (client)
using (var clientStream = client.GetStream())
{
try
{
if (Options.DropAfterAccept)
{
return;
}
// Greeting: VER NMETHODS METHODS[]
var versionAndCount = new byte[2];
await ReadExactlyAsync(clientStream, versionAndCount, _cts.Token).ConfigureAwait(false);
if (versionAndCount[0] != 0x05)
{
return;
}
var methods = new byte[versionAndCount[1]];
await ReadExactlyAsync(clientStream, methods, _cts.Token).ConfigureAwait(false);
if (Options.GreetingResponseOverride != null)
{
await clientStream.WriteAsync(Options.GreetingResponseOverride, _cts.Token).ConfigureAwait(false);
if (Options.DropAfterGreetingResponse)
{
return;
}
}
else
{
byte method = SelectMethod(methods);
await clientStream.WriteAsync(new byte[] { 0x05, method }, _cts.Token).ConfigureAwait(false);
if (method == 0xFF)
{
return;
}
if (method == 0x02)
{
if (!await HandleAuthAsync(clientStream).ConfigureAwait(false))
{
return;
}
}
}
// CONNECT request: VER CMD RSV ATYP DST.ADDR DST.PORT
var header = new byte[4];
await ReadExactlyAsync(clientStream, header, _cts.Token).ConfigureAwait(false);
if (header[0] != 0x05)
{
// RFC 1928 §4: VER must be X'05' in every message.
return;
}
if (header[1] != 0x01)
{
// RFC 1928 §4: only CONNECT (0x01) is supported by this fake server. BIND (0x02)
// and UDP ASSOCIATE (0x03) are signaled as unsupported per spec via REP=0x07.
await WriteReplyAsync(clientStream, 0x07, Options.ReplyReservedByte, Options.ReplyAddressType, Options.ReplyBindHost, Options.ReplyBindPort).ConfigureAwait(false);
return;
}
string targetHost;
IPAddress targetIp = null;
byte atyp = header[3];
switch (atyp)
{
case 0x01:
{
var addr = new byte[4];
await ReadExactlyAsync(clientStream, addr, _cts.Token).ConfigureAwait(false);
targetIp = new IPAddress(addr);
targetHost = targetIp.ToString();
break;
}
case 0x03:
{
var lenBuf = new byte[1];
await ReadExactlyAsync(clientStream, lenBuf, _cts.Token).ConfigureAwait(false);
var hostBytes = new byte[lenBuf[0]];
await ReadExactlyAsync(clientStream, hostBytes, _cts.Token).ConfigureAwait(false);
targetHost = Encoding.UTF8.GetString(hostBytes);
break;
}
case 0x04:
{
var addr = new byte[16];
await ReadExactlyAsync(clientStream, addr, _cts.Token).ConfigureAwait(false);
targetIp = new IPAddress(addr);
targetHost = targetIp.ToString();
break;
}
default:
await WriteReplyAsync(clientStream, 0x08, Options.ReplyReservedByte, Options.ReplyAddressType, Options.ReplyBindHost, Options.ReplyBindPort).ConfigureAwait(false);
return;
}
var portBuf = new byte[2];
await ReadExactlyAsync(clientStream, portBuf, _cts.Token).ConfigureAwait(false);
var targetPort = BinaryPrimitives.ReadUInt16BigEndian(portBuf);
lock (_recordedLock)
{
_recordedConnects.Add(new RecordedConnect(atyp, targetHost, targetPort));
}
if (Options.ConnectReplyCode != 0x00)
{
await WriteReplyAsync(clientStream, Options.ConnectReplyCode, Options.ReplyReservedByte, Options.ReplyAddressType, Options.ReplyBindHost, Options.ReplyBindPort).ConfigureAwait(false);
return;
}
// Open the destination connection and start relaying.
TcpClient upstream;
try
{
upstream = new TcpClient();
if (targetIp != null)
{
await upstream.ConnectAsync(targetIp, targetPort, _cts.Token).ConfigureAwait(false);
}
else
{
await upstream.ConnectAsync(targetHost, targetPort, _cts.Token).ConfigureAwait(false);
}
}
catch (Exception)
{
await WriteReplyAsync(clientStream, 0x04, Options.ReplyReservedByte, Options.ReplyAddressType, Options.ReplyBindHost, Options.ReplyBindPort).ConfigureAwait(false);
return;
}
using (upstream)
using (var upstreamStream = upstream.GetStream())
{
await WriteReplyAsync(clientStream, 0x00, Options.ReplyReservedByte, Options.ReplyAddressType, Options.ReplyBindHost, Options.ReplyBindPort).ConfigureAwait(false);
var c2u = RelayAsync(clientStream, upstreamStream, _cts.Token);
var u2c = RelayAsync(upstreamStream, clientStream, _cts.Token);
await Task.WhenAny(c2u, u2c).ConfigureAwait(false);
}
}
catch (Exception)
{
// swallow — diagnostic-only
}
}
}
byte SelectMethod(byte[] offered)
{
var allowedMethods = Options.AllowedMethods;
foreach (var m in offered)
{
foreach (var allowed in allowedMethods)
{
if (m == allowed)
{
return m;
}
}
}
return 0xFF;
}
async Task<bool> HandleAuthAsync(NetworkStream stream)
{
var header = new byte[2];
await ReadExactlyAsync(stream, header, _cts.Token).ConfigureAwait(false);
if (header[0] != 0x01)
{
return false;
}
var ulen = header[1];
var userBytes = new byte[ulen];
await ReadExactlyAsync(stream, userBytes, _cts.Token).ConfigureAwait(false);
var plenBuf = new byte[1];
await ReadExactlyAsync(stream, plenBuf, _cts.Token).ConfigureAwait(false);
var passBytes = new byte[plenBuf[0]];
await ReadExactlyAsync(stream, passBytes, _cts.Token).ConfigureAwait(false);
var user = Encoding.UTF8.GetString(userBytes);
var pass = Encoding.UTF8.GetString(passBytes);
var ok = Options.ExpectedUsername == user && Options.ExpectedPassword == pass;
if (Options.ForceAuthFailure)
{
ok = false;
}
await stream.WriteAsync(new byte[] { 0x01, ok ? (byte)0x00 : (byte)0x01 }, _cts.Token).ConfigureAwait(false);
return ok;
}
static async Task WriteReplyAsync(NetworkStream stream, byte rep, byte replyReserved, byte replyAtyp, string replyHost, ushort replyPort)
{
// VER REP RSV ATYP BND.ADDR BND.PORT
//
// RFC 1928 §6 says BND.ADDR contains the "associated IP address" (i.e. the server-side
// endpoint it used to reach the destination). Most real SOCKS5 servers reply with
// ATYP=0x01 (IPv4) + a zeroed address when they have nothing meaningful to report,
// which is the safe default we use here. Tests can override the reply ATYP via
// FakeSocks5ServerOptions.ReplyAddressType to exercise the client's reply parser.
switch (replyAtyp)
{
case 0x04:
{
var reply = new byte[4 + 16 + 2];
reply[0] = 0x05;
reply[1] = rep;
reply[2] = replyReserved;
reply[3] = 0x04;
if (!string.IsNullOrEmpty(replyHost) && IPAddress.TryParse(replyHost, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6)
{
var bytes = ip.GetAddressBytes();
Buffer.BlockCopy(bytes, 0, reply, 4, 16);
}
BinaryPrimitives.WriteUInt16BigEndian(reply.AsSpan(20, 2), replyPort);
await stream.WriteAsync(reply).ConfigureAwait(false);
return;
}
case 0x03:
{
var host = string.IsNullOrEmpty(replyHost) ? "0.0.0.0" : replyHost;
var hostBytes = Encoding.UTF8.GetBytes(host);
if (hostBytes.Length > 255)
{
Array.Resize(ref hostBytes, 255);
}
var reply = new byte[4 + 1 + hostBytes.Length + 2];
reply[0] = 0x05;
reply[1] = rep;
reply[2] = replyReserved;
reply[3] = 0x03;
reply[4] = (byte)hostBytes.Length;
Buffer.BlockCopy(hostBytes, 0, reply, 5, hostBytes.Length);
BinaryPrimitives.WriteUInt16BigEndian(reply.AsSpan(5 + hostBytes.Length, 2), replyPort);
await stream.WriteAsync(reply).ConfigureAwait(false);
return;
}
default:
{
var reply = new byte[4 + 4 + 2];
reply[0] = 0x05;
reply[1] = rep;
reply[2] = replyReserved;
reply[3] = 0x01;
if (!string.IsNullOrEmpty(replyHost) && IPAddress.TryParse(replyHost, out var ip) && ip.AddressFamily == AddressFamily.InterNetwork)
{
var bytes = ip.GetAddressBytes();
Buffer.BlockCopy(bytes, 0, reply, 4, 4);
}
BinaryPrimitives.WriteUInt16BigEndian(reply.AsSpan(8, 2), replyPort);
await stream.WriteAsync(reply).ConfigureAwait(false);
return;
}
}
}
static async Task RelayAsync(NetworkStream source, NetworkStream destination, CancellationToken cancellationToken)
{
var buffer = new byte[16 * 1024];
try
{
while (true)
{
var read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
return;
}
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
}
}
catch (Exception)
{
// ignore
}
}
static async Task ReadExactlyAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken)
{
var offset = 0;
while (offset < buffer.Length)
{
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
throw new EndOfStreamException();
}
offset += read;
}
}
public sealed record RecordedConnect(byte AddressType, string Host, int Port);
}
internal sealed class FakeSocks5ServerOptions
{
/// <summary>
/// The SOCKS5 auth methods the server will accept, in preference order matched against the
/// client's offered methods. Defaults to no-auth only.
/// </summary>
public byte[] AllowedMethods { get; set; } = [0x00];
public string ExpectedUsername { get; set; }
public string ExpectedPassword { get; set; }
/// <summary>
/// Forces the username/password sub-negotiation to return STATUS=0x01 regardless of the
/// supplied credentials. Useful for tests that exercise the auth-failure path.
/// </summary>
public bool ForceAuthFailure { get; set; }
/// <summary>
/// SOCKS5 reply code for the CONNECT response. <c>0x00</c> means "succeeded" and the
/// server will additionally bridge bytes to the destination.
/// </summary>
public byte ConnectReplyCode { get; set; }
/// <summary>
/// Reserved byte included in the CONNECT reply. Defaults to <c>0x00</c> per RFC 1928.
/// </summary>
public byte ReplyReservedByte { get; set; }
/// <summary>
/// When set, the server sends this byte sequence instead of the regular greeting response.
/// Used to simulate malformed responses (e.g. wrong version).
/// </summary>
public byte[] GreetingResponseOverride { get; set; }
/// <summary>
/// If <c>true</c>, closes the connection right after writing the greeting response
/// override. Used together with <see cref="GreetingResponseOverride"/> for protocol-error
/// tests.
/// </summary>
public bool DropAfterGreetingResponse { get; set; }
/// <summary>
/// If <c>true</c>, the server closes the connection immediately after accepting (before
/// reading the greeting). Used to simulate proxy mid-handshake disconnects.
/// </summary>
public bool DropAfterAccept { get; set; }
/// <summary>
/// Address type used when constructing CONNECT replies. Defaults to <c>0x01</c> (IPv4),
/// which matches the behavior of real SOCKS5 servers — they always reply with a resolved
/// address, regardless of how the client expressed the destination. Override to
/// <c>0x03</c> or <c>0x04</c> to exercise the client's reply parser.
/// </summary>
public byte ReplyAddressType { get; set; } = 0x01;
/// <summary>
/// Bound host included in the CONNECT reply (BND.ADDR). Format depends on
/// <see cref="ReplyAddressType"/>: IPv4/IPv6 textual representation for ATYP 0x01/0x04,
/// a domain name for ATYP 0x03. When <c>null</c> or empty the server writes a zeroed
/// address (0.0.0.0, ::, or "0.0.0.0" respectively).
/// </summary>
public string ReplyBindHost { get; set; }
/// <summary>
/// Bound port included in the CONNECT reply (BND.PORT). Defaults to <c>0</c>, which
/// is what real SOCKS5 servers return when no meaningful port is available.
/// </summary>
public ushort ReplyBindPort { get; set; }
}
@@ -0,0 +1,5 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
global using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<LangVersion>default</LangVersion>
<Nullable>disable</Nullable>
<NoWarn>$(NoWarn);CA1707;CA1859</NoWarn>
<EnableMSTestRunner>true</EnableMSTestRunner>
<RootNamespace>MQTTnet.Tests.Extensions.Socks5</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Server\MQTTnet.Server.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.Socks5\MQTTnet.Extensions.Socks5.csproj" />
</ItemGroup>
<ItemGroup>
<None Include=".\..\..\global.json"
Link="global.json"
CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
</Project>
@@ -0,0 +1,566 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Exceptions;
using MQTTnet.Extensions.Socks5;
using MQTTnet.Server;
namespace MQTTnet.Tests.Extensions.Socks5;
[TestClass]
public sealed class Socks5MqttClient_Tests
{
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_No_Auth_PubSub_Works()
{
Socks5TestHelper.BrokerHandle broker = null;
FakeSocks5Server proxy = null;
IMqttClient client = null;
try
{
broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
client = new MqttClientFactory().CreateMqttClient();
var receivedTopic = string.Empty;
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
client.ApplicationMessageReceivedAsync += e =>
{
receivedTopic = e.ApplicationMessage.Topic;
received.TrySetResult(Encoding.UTF8.GetString(e.ApplicationMessage.Payload.ToArray()));
return Task.CompletedTask;
};
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
await client.ConnectAsync(options, CancellationToken.None);
Assert.IsTrue(client.IsConnected);
await client.SubscribeAsync("topic/socks5");
await client.PublishStringAsync("topic/socks5", "hello-socks5");
var payload = await received.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.AreEqual("topic/socks5", receivedTopic);
Assert.AreEqual("hello-socks5", payload);
Assert.HasCount(1, proxy.RecordedConnects);
Assert.AreEqual(brokerPort, proxy.RecordedConnects[0].Port);
}
finally
{
if (client != null)
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
client.Dispose();
}
if (proxy != null)
{
await proxy.DisposeAsync();
}
if (broker != null)
{
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_With_UsernamePassword_Works()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
AllowedMethods = [0x02],
ExpectedUsername = "alice",
ExpectedPassword = "s3cr3t"
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port).WithCredentials("alice", "s3cr3t"))
.Build();
try
{
await client.ConnectAsync(options, CancellationToken.None);
Assert.IsTrue(client.IsConnected);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_With_Wrong_Credentials_Throws_AuthFailed()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
AllowedMethods = [0x02],
ForceAuthFailure = true
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port).WithCredentials("alice", "wrong"))
.Build();
try
{
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyAuthFailed, ex.ErrorCode);
}
finally
{
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Method_Rejected_Throws_AuthMethodRejected()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
AllowedMethods = [0x01] // GSSAPI which the client never offers.
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
try
{
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyAuthMethodRejected, ex.ErrorCode);
}
finally
{
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Broker_Host_Unreachable_Maps_To_HostUnreachable()
{
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
ConnectReplyCode = 0x04
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("broker.invalid", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyHostUnreachable, ex.ErrorCode);
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Broker_Connection_Refused_Maps_To_ConnectionRefused()
{
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
ConnectReplyCode = 0x05
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyConnectionRefused, ex.ErrorCode);
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Proxy_Port_Closed_Throws_ProxyUnreachable()
{
// Bind a temporary listener just to grab a free port and immediately release it.
var probe = new TcpListener(IPAddress.Loopback, 0);
probe.Start();
var freePort = ((IPEndPoint)probe.LocalEndpoint).Port;
probe.Stop();
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(freePort).WithHandshakeTimeout(TimeSpan.FromSeconds(5)))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyUnreachable, ex.ErrorCode);
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Proxy_Host_Does_Not_Resolve_Throws_ProxyUnreachable()
{
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1883)
.WithSocks5Proxy(p => p.WithHost("nonexistent.proxy.invalid").WithPort(1080).WithHandshakeTimeout(TimeSpan.FromSeconds(5)))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyUnreachable, ex.ErrorCode);
}
[TestMethod]
public async Task Remote_Dns_Enabled_Sends_DomainName_To_Proxy()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("localhost", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port).WithResolveDnsRemotely(true))
.Build();
try
{
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
Assert.HasCount(1, proxy.RecordedConnects);
Assert.AreEqual<byte>(0x03, proxy.RecordedConnects[0].AddressType);
Assert.AreEqual("localhost", proxy.RecordedConnects[0].Host);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Remote_Dns_Disabled_Sends_Ipv4_To_Proxy()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("localhost", brokerPort)
.WithSocks5Proxy(p => p
.WithHost("127.0.0.1")
.WithPort(proxy.Port)
.WithResolveDnsRemotely(false)
.WithAddressFamily(AddressFamily.InterNetwork))
.Build();
try
{
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
Assert.HasCount(1, proxy.RecordedConnects);
Assert.AreEqual<byte>(0x01, proxy.RecordedConnects[0].AddressType);
Assert.AreEqual("127.0.0.1", proxy.RecordedConnects[0].Host);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Ipv6_IpEndPoint_Sends_Ipv6_AddressType()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
using var client = new MqttClientFactory().CreateMqttClient();
// Loopback (::1) is reachable on practically every CI runner.
var options = new MqttClientOptionsBuilder()
.WithEndPoint(new IPEndPoint(IPAddress.IPv6Loopback, brokerPort))
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
try
{
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
Assert.HasCount(1, proxy.RecordedConnects);
Assert.AreEqual<byte>(0x04, proxy.RecordedConnects[0].AddressType);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Cancellation_During_Handshake_Throws_OperationCanceled()
{
// Proxy that accepts the TCP connection but never replies to the greeting.
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
DropAfterAccept = true
});
using var client = new MqttClientFactory().CreateMqttClient();
using var cts = new CancellationTokenSource();
await cts.CancelAsync();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
await Assert.ThrowsAsync<OperationCanceledException>(() => client.ConnectAsync(options, cts.Token));
}
[TestMethod]
public async Task Greeting_Returns_Wrong_Version_Throws_ProtocolError()
{
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
GreetingResponseOverride = [0x04, 0x00],
DropAfterGreetingResponse = true
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyProtocolError, ex.ErrorCode);
}
[TestMethod]
public async Task Connect_Response_With_NonZero_Reserved_Byte_Throws_ProtocolError()
{
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
ReplyReservedByte = 0x01
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyProtocolError, ex.ErrorCode);
}
[TestMethod]
public async Task Reconnect_After_Proxy_Disconnect_Works()
{
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
try
{
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
// Force the broker side to drop the client by stopping & restarting it (proxy
// forwarding closes when the upstream connection closes).
await broker.Server.StopAsync();
broker.Server.Dispose();
await Task.Delay(500);
broker = await Socks5TestHelper.StartBrokerAsync();
var newPort = Socks5TestHelper.GetBrokerPort(broker);
options = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", newPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
// Two distinct CONNECT requests should have hit the proxy.
Assert.IsGreaterThanOrEqualTo(2, proxy.RecordedConnects.Count);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
[TestMethod]
public async Task Connect_Tls_Via_Socks5_PubSub_Works()
{
var tlsBroker = await Socks5TestHelper.StartTlsBrokerAsync();
try
{
using (tlsBroker.Certificate)
{
var brokerPort = Socks5TestHelper.GetTlsBrokerPort(tlsBroker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions());
using var client = new MqttClientFactory().CreateMqttClient();
var receivedTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
client.ApplicationMessageReceivedAsync += e =>
{
receivedTcs.TrySetResult(Encoding.UTF8.GetString(e.ApplicationMessage.Payload.ToArray()));
return Task.CompletedTask;
};
var options = new MqttClientOptionsBuilder()
.WithTcpServer("localhost", brokerPort)
.WithTlsOptions(o => o
.WithSslProtocols(System.Security.Authentication.SslProtocols.Tls12)
.WithCertificateValidationHandler(_ => true))
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
await client.ConnectAsync(options);
Assert.IsTrue(client.IsConnected);
await client.SubscribeAsync("tls/socks5");
await client.PublishStringAsync("tls/socks5", "tls-hello");
var payload = await receivedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.AreEqual("tls-hello", payload);
// Validate that the proxy saw a CONNECT with domain forwarding.
Assert.HasCount(1, proxy.RecordedConnects);
Assert.AreEqual<byte>(0x03, proxy.RecordedConnects[0].AddressType);
Assert.AreEqual("localhost", proxy.RecordedConnects[0].Host);
try { await client.DisconnectAsync(); } catch { /* ignore */ }
}
}
finally
{
await tlsBroker.Server.StopAsync();
tlsBroker.Server.Dispose();
}
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_When_Command_Not_Supported_Maps_To_CommandNotSupported()
{
// Server replies with REP=0x07 ("Command not supported / protocol error") per RFC 1928 §6.
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
ConnectReplyCode = 0x07
});
using var client = new MqttClientFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("broker.example", 1883)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
var ex = await Assert.ThrowsExactlyAsync<MqttProxyException>(() => client.ConnectAsync(options));
Assert.AreEqual(MqttProxyErrorCode.ProxyCommandNotSupported, ex.ErrorCode);
}
[TestMethod]
public async Task Connect_Tcp_Via_Socks5_Parses_DomainName_BindAddress_In_Reply()
{
// RFC 1928 §6 allows ATYP=0x03 (DOMAINNAME) in the CONNECT reply with a variable-length
// BND.ADDR. Verify the client correctly parses the length-prefixed bind address and
// completes the handshake even when the server uses a non-IP BND.ADDR.
var broker = await Socks5TestHelper.StartBrokerAsync();
var brokerPort = Socks5TestHelper.GetBrokerPort(broker);
await using var proxy = new FakeSocks5Server(new FakeSocks5ServerOptions
{
ReplyAddressType = 0x03,
ReplyBindHost = "proxy.example.internal",
ReplyBindPort = 4242
});
using var client = new MqttClientFactory().CreateMqttClient();
var connectOptions = new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", brokerPort)
.WithSocks5Proxy(p => p.WithHost("127.0.0.1").WithPort(proxy.Port))
.Build();
try
{
await client.ConnectAsync(connectOptions);
Assert.IsTrue(client.IsConnected);
}
finally
{
try { await client.DisconnectAsync(); } catch { /* ignore */ }
await broker.Server.StopAsync();
broker.Server.Dispose();
}
}
}
@@ -0,0 +1,112 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .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.Extensions.Socks5;
namespace MQTTnet.Tests.Extensions.Socks5;
[TestClass]
public sealed class Socks5ProxyOptionsValidation_Tests
{
[TestMethod]
public void Builder_Build_Without_Host_Throws()
{
var builder = new Socks5ProxyOptionsBuilder();
Assert.ThrowsExactly<InvalidOperationException>(() => builder.Build());
}
[TestMethod]
public void Builder_Build_With_Invalid_Port_Throws()
{
var builder = new Socks5ProxyOptionsBuilder()
.WithHost("127.0.0.1")
.WithPort(0);
Assert.ThrowsExactly<InvalidOperationException>(() => builder.Build());
}
[TestMethod]
public void Builder_Build_With_NonPositive_HandshakeTimeout_Throws()
{
var builder = new Socks5ProxyOptionsBuilder()
.WithHost("127.0.0.1")
.WithHandshakeTimeout(TimeSpan.Zero);
Assert.ThrowsExactly<InvalidOperationException>(() => builder.Build());
}
[TestMethod]
public void Builder_Build_With_Password_But_Without_Username_Throws()
{
var builder = new Socks5ProxyOptionsBuilder()
.WithHost("127.0.0.1")
.WithCredentials(null, new byte[] { 0x01, 0x02 });
Assert.ThrowsExactly<InvalidOperationException>(() => builder.Build());
}
[TestMethod]
public void Builder_Build_With_Username_But_Without_Password_Throws()
{
var builder = new Socks5ProxyOptionsBuilder()
.WithHost("127.0.0.1")
.WithCredentials("alice", Array.Empty<byte>());
Assert.ThrowsExactly<InvalidOperationException>(() => builder.Build());
}
[TestMethod]
public void StreamProvider_Ctor_With_Invalid_Port_Throws()
{
var options = new Socks5ProxyOptions
{
Host = "127.0.0.1",
Port = 70000
};
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => _ = new Socks5StreamProvider(options));
}
[TestMethod]
public void StreamProvider_Ctor_With_NonPositive_HandshakeTimeout_Throws()
{
var options = new Socks5ProxyOptions
{
Host = "127.0.0.1",
Port = 1080,
HandshakeTimeout = TimeSpan.Zero
};
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => _ = new Socks5StreamProvider(options));
}
[TestMethod]
public void StreamProvider_Ctor_With_Password_But_Without_Username_Throws()
{
var options = new Socks5ProxyOptions
{
Host = "127.0.0.1",
Port = 1080,
Password = new byte[] { 0x01 }
};
Assert.ThrowsExactly<ArgumentException>(() => _ = new Socks5StreamProvider(options));
}
[TestMethod]
public void StreamProvider_Ctor_With_Username_But_Without_Password_Throws()
{
var options = new Socks5ProxyOptions
{
Host = "127.0.0.1",
Port = 1080,
Username = "alice",
Password = Array.Empty<byte>()
};
Assert.ThrowsExactly<ArgumentException>(() => _ = new Socks5StreamProvider(options));
}
}
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using MQTTnet.Certificates;
using MQTTnet.Server;
namespace MQTTnet.Tests.Extensions.Socks5;
internal static class Socks5TestHelper
{
public static async Task<BrokerHandle> StartBrokerAsync()
{
var factory = new MqttServerFactory();
var options = factory.CreateServerOptionsBuilder()
.WithDefaultEndpoint()
.WithDefaultEndpointPort(0)
.Build();
var server = factory.CreateMqttServer(options);
await server.StartAsync();
return new BrokerHandle(server, options);
}
public static int GetBrokerPort(BrokerHandle handle)
{
return handle.Options.DefaultEndpointOptions.Port;
}
public static async Task<TlsBrokerHandle> StartTlsBrokerAsync()
{
var certificate = CreateSelfSignedServerCertificate();
var factory = new MqttServerFactory();
var options = factory.CreateServerOptionsBuilder()
.WithoutDefaultEndpoint()
.WithEncryptedEndpoint()
.WithEncryptedEndpointPort(0)
.WithEncryptionCertificate(new InMemoryCertificateProvider(certificate))
.Build();
var server = factory.CreateMqttServer(options);
await server.StartAsync();
return new TlsBrokerHandle(server, options, certificate);
}
public static int GetTlsBrokerPort(TlsBrokerHandle handle)
{
return handle.Options.TlsEndpointOptions.Port;
}
public static X509Certificate2 CreateSelfSignedServerCertificate()
{
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddIpAddress(IPAddress.Loopback);
sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
sanBuilder.AddDnsName("localhost");
using var rsa = RSA.Create(2048);
var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
certRequest.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
certRequest.CertificateExtensions.Add(sanBuilder.Build());
using var cert = certRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-10), DateTimeOffset.UtcNow.AddMinutes(60));
#pragma warning disable SYSLIB0057
return new X509Certificate2(
cert.Export(X509ContentType.Pfx),
(string)null,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
#pragma warning restore SYSLIB0057
}
internal sealed record BrokerHandle(MqttServer Server, MqttServerOptions Options);
internal sealed record TlsBrokerHandle(MqttServer Server, MqttServerOptions Options, X509Certificate2 Certificate);
sealed class InMemoryCertificateProvider : ICertificateProvider
{
readonly X509Certificate2 _certificate;
public InMemoryCertificateProvider(X509Certificate2 certificate)
{
_certificate = certificate;
}
public X509Certificate2 GetCertificate() => _certificate;
}
}
@@ -6,6 +6,7 @@ using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
using MQTTnet.Server.EnhancedAuthentication;
@@ -143,7 +144,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass
var client = await testEnvironment.ConnectClient();
var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build();
var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", Encoding.UTF8.GetBytes("test_value")).Build();
// Perform a clean disconnect.
await client.DisconnectAsync(disconnectOptions);
@@ -154,7 +155,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass
Assert.IsNotNull(eventArgs.UserProperties);
Assert.HasCount(1, eventArgs.UserProperties);
Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name);
Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value);
Assert.AreEqual("test_value", eventArgs.UserProperties[0].ReadValueAsString());
}
sealed class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler
@@ -260,7 +261,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass
server.ValidatingConnectionAsync += args =>
{
args.ResponseUserProperties = [new("Property", "Value")];
args.ResponseUserProperties = [new("Property", Encoding.UTF8.GetBytes("Value"))];
args.ReasonCode = MqttConnectReasonCode.QuotaExceeded;
@@ -274,7 +275,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass
Assert.IsNotNull(response);
Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode);
Assert.AreEqual("Property", response.UserProperties[0].Name);
Assert.AreEqual("Value", response.UserProperties[0].Value);
Assert.AreEqual("Value", response.UserProperties[0].ReadValueAsString());
}
[TestMethod]
@@ -5,8 +5,12 @@
using System.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics.Logger;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -454,6 +458,34 @@ public sealed class MqttClient_Tests : BaseTestClass
}
}
[TestMethod]
public async Task KeepAlive_Ping_Send_Timeout_Raises_Disconnected()
{
var adapterFactory = new KeepAliveTimeoutAdapterFactory();
var disconnected = new TaskCompletionSource<MqttClientDisconnectedEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
using var client = new MqttClientFactory().CreateMqttClient(adapterFactory);
client.DisconnectedAsync += eventArgs =>
{
disconnected.TrySetResult(eventArgs);
return CompletedTask.Instance;
};
var options = new MqttClientOptionsBuilder()
.WithTcpServer("localhost")
.WithKeepAlivePeriod(TimeSpan.FromSeconds(1))
.Build();
await client.ConnectAsync(options);
await adapterFactory.Adapter.PingRequestReceived.Task.WaitAsync(TimeSpan.FromSeconds(3));
var disconnectedEventArgs = await disconnected.Task.WaitAsync(TimeSpan.FromSeconds(3));
Assert.IsTrue(disconnectedEventArgs.ClientWasConnected);
Assert.IsInstanceOfType<OperationCanceledException>(disconnectedEventArgs.Exception);
Assert.IsFalse(client.IsConnected);
}
[TestMethod]
public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler()
{
@@ -870,4 +902,189 @@ public sealed class MqttClient_Tests : BaseTestClass
Assert.IsTrue(client1.TryPingAsync().GetAwaiter().GetResult());
Assert.IsFalse(disconnectedFired);
}
}
// Regression test for issue #2079.
// A late PUBACK (one whose matching publish request was already cancelled or timed
// out client-side) must NOT raise a protocol violation and must NOT disconnect the
// client. Before the fix, the client received the late PUBACK, failed to find a
// waiter and threw a MqttProtocolViolationException, tearing down the connection.
[TestMethod]
[DataRow(MqttQualityOfServiceLevel.AtLeastOnce)]
[DataRow(MqttQualityOfServiceLevel.ExactlyOnce)]
public async Task Cancelled_Publish_Does_Not_Disconnect_Client(MqttQualityOfServiceLevel qos)
{
using var testEnvironment = new TestEnvironment(TestContext);
var server = await testEnvironment.StartServer();
// Delay the broker's acknowledgement long enough for the publish caller to give
// up. When the caller's cancellation token fires the awaitable is removed; the
// (slightly later) PUBACK / PUBREC then arrives without a matching waiter.
server.InterceptingPublishAsync += async e =>
{
await Task.Delay(TimeSpan.FromSeconds(2), CancellationToken.None);
};
var client = await testEnvironment.ConnectClient();
var disconnected = false;
client.DisconnectedAsync += _ =>
{
disconnected = true;
return CompletedTask.Instance;
};
var message = new MqttApplicationMessageBuilder().WithTopic("late-ack").WithQualityOfServiceLevel(qos).Build();
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
{
await Assert.ThrowsExactlyAsync<MqttCommunicationTimedOutException>(() => client.PublishAsync(message, cts.Token));
}
// Wait long enough for the delayed broker acknowledgement to come back.
await Task.Delay(TimeSpan.FromSeconds(3));
Assert.IsFalse(disconnected, "Client must not disconnect when a late acknowledgement arrives.");
Assert.IsTrue(client.IsConnected, "Client must still be connected after a late acknowledgement.");
// The connection must remain usable for further publishes.
var followUp = new MqttApplicationMessageBuilder().WithTopic("late-ack").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce).Build();
await client.PublishAsync(followUp);
}
// Regression test for issue #2078.
// The MqttClient must support concurrent publishes from multiple threads without
// dropping messages, mismatching acknowledgements or disconnecting the client.
[TestMethod]
[DataRow(MqttQualityOfServiceLevel.AtMostOnce)]
[DataRow(MqttQualityOfServiceLevel.AtLeastOnce)]
[DataRow(MqttQualityOfServiceLevel.ExactlyOnce)]
public async Task Concurrent_Publish_From_Multiple_Threads(MqttQualityOfServiceLevel qos)
{
const int Threads = 8;
const int MessagesPerThread = 250;
using var testEnvironment = new TestEnvironment(TestContext);
await testEnvironment.StartServer();
var publisher = await testEnvironment.ConnectClient();
var subscriber = await testEnvironment.ConnectClient();
var receivedCount = 0;
subscriber.ApplicationMessageReceivedAsync += _ =>
{
Interlocked.Increment(ref receivedCount);
return CompletedTask.Instance;
};
await subscriber.SubscribeAsync("concurrent/#", qos);
var disconnected = false;
publisher.DisconnectedAsync += _ =>
{
disconnected = true;
return CompletedTask.Instance;
};
var tasks = Enumerable.Range(0, Threads).Select(threadIndex => Task.Run(async () =>
{
for (var i = 0; i < MessagesPerThread; i++)
{
var message = new MqttApplicationMessageBuilder()
.WithTopic($"concurrent/{threadIndex}/{i}")
.WithPayload(BitConverter.GetBytes(i))
.WithQualityOfServiceLevel(qos)
.Build();
await publisher.PublishAsync(message);
}
})).ToArray();
await Task.WhenAll(tasks);
// Allow the broker to deliver any in-flight messages to the subscriber.
SpinWait.SpinUntil(() => receivedCount >= Threads * MessagesPerThread, TimeSpan.FromSeconds(30));
Assert.IsFalse(disconnected, "Publisher must not disconnect during concurrent publishes.");
Assert.IsTrue(publisher.IsConnected);
Assert.AreEqual(Threads * MessagesPerThread, receivedCount);
}
sealed class KeepAliveTimeoutAdapterFactory : IMqttClientAdapterFactory
{
public KeepAliveTimeoutAdapter Adapter { get; private set; }
public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger)
{
Adapter = new KeepAliveTimeoutAdapter(options.ProtocolVersion, options.WriterBufferSize, options.WriterBufferSizeMax);
return Adapter;
}
}
sealed class KeepAliveTimeoutAdapter : IMqttChannelAdapter
{
int _connAckReturned;
public KeepAliveTimeoutAdapter(MqttProtocolVersion protocolVersion, int writerBufferSize, int writerBufferSizeMax)
{
PacketFormatterAdapter = new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(writerBufferSize, writerBufferSizeMax));
}
public long BytesReceived { get; }
public long BytesSent { get; }
public X509Certificate2 ClientCertificate { get; }
public EndPoint RemoteEndPoint { get; } = new DnsEndPoint("localhost", 1883);
public EndPoint LocalEndPoint { get; } = new DnsEndPoint("localhost", 0);
public bool IsSecureConnection { get; }
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public TaskCompletionSource PingRequestReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task ConnectAsync(CancellationToken cancellationToken)
{
return CompletedTask.Instance;
}
public Task DisconnectAsync(CancellationToken cancellationToken)
{
return CompletedTask.Instance;
}
public void Dispose()
{
}
public async Task<MqttPacket> ReceivePacketAsync(CancellationToken cancellationToken)
{
if (Interlocked.Exchange(ref _connAckReturned, 1) == 0)
{
return new MqttConnAckPacket
{
ReturnCode = MqttConnectReturnCode.ConnectionAccepted
};
}
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
return null;
}
public void ResetStatistics()
{
}
public Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
{
if (packet is MqttPingReqPacket)
{
PingRequestReceived.TrySetResult();
return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
return CompletedTask.Instance;
}
}
}
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Buffers;
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Packets;
@@ -45,7 +46,7 @@ public sealed class MqttPacketSerialization_V3_Tests
SubscriptionIdentifiersAvailable = true,
TopicAliasMaximum = 963,
WildcardSubscriptionAvailable = true,
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V311);
@@ -96,7 +97,7 @@ public sealed class MqttPacketSerialization_V3_Tests
SubscriptionIdentifiersAvailable = true,
TopicAliasMaximum = 963,
WildcardSubscriptionAvailable = true,
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V310);
@@ -152,8 +153,8 @@ public sealed class MqttPacketSerialization_V3_Tests
WillResponseTopic = "WillResponseTopic",
WillMessageExpiryInterval = 542,
WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData,
UserProperties = [new("Foo", "Bar")],
WillUserProperties = [new("WillFoo", "WillBar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))],
WillUserProperties = [new("WillFoo", Encoding.UTF8.GetBytes("WillBar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V311);
@@ -195,7 +196,7 @@ public sealed class MqttPacketSerialization_V3_Tests
ReasonString = "ReasonString",
ServerReference = "ServerReference",
SessionExpiryInterval = 234,
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V311);
@@ -235,7 +236,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers,
ReasonString = "ReasonString",
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V311);
@@ -254,7 +255,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound,
ReasonString = "ReasonString",
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V311);
@@ -284,7 +285,7 @@ public sealed class MqttPacketSerialization_V3_Tests
SubscriptionIdentifiers = [123],
MessageExpiryInterval = 38,
PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData,
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V311);
@@ -313,7 +314,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubRecReasonCode.UnspecifiedError,
ReasonString = "ReasonString",
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V311);
@@ -333,7 +334,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound,
ReasonString = "ReasonString",
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V311);
@@ -353,7 +354,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonString = "ReasonString",
ReasonCodes = [MqttSubscribeReasonCode.GrantedQoS1],
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V311);
@@ -383,7 +384,7 @@ public sealed class MqttPacketSerialization_V3_Tests
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce
}
],
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V311);
@@ -407,7 +408,7 @@ public sealed class MqttPacketSerialization_V3_Tests
PacketIdentifier = 123,
ReasonCodes = [MqttUnsubscribeReasonCode.ImplementationSpecificError],
ReasonString = "ReasonString",
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V311);
@@ -425,7 +426,7 @@ public sealed class MqttPacketSerialization_V3_Tests
{
PacketIdentifier = 123,
TopicFilters = ["TopicFilter1"],
UserProperties = [new("Foo", "Bar")]
UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V311);
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Buffers;
using System.Text;
using MQTTnet.Formatter;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -32,7 +33,7 @@ public sealed class MqttPacketSerialization_V5_Tests
AuthenticationMethod = "AuthenticationMethod",
ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication,
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V500);
@@ -68,7 +69,7 @@ public sealed class MqttPacketSerialization_V5_Tests
SubscriptionIdentifiersAvailable = true,
TopicAliasMaximum = 963,
WildcardSubscriptionAvailable = true,
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V500);
@@ -124,8 +125,8 @@ public sealed class MqttPacketSerialization_V5_Tests
WillResponseTopic = "WillResponseTopic",
WillMessageExpiryInterval = 542,
WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData,
UserProperties = [new MqttUserProperty("Foo", "Bar")],
WillUserProperties = [new MqttUserProperty("WillFoo", "WillBar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))],
WillUserProperties = [new MqttUserProperty("WillFoo", Encoding.UTF8.GetBytes("WillBar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V500);
@@ -167,7 +168,7 @@ public sealed class MqttPacketSerialization_V5_Tests
ReasonString = "ReasonString",
ServerReference = "ServerReference",
SessionExpiryInterval = 234,
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V500);
@@ -207,7 +208,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers,
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V500);
@@ -226,7 +227,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound,
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V500);
@@ -255,7 +256,7 @@ public sealed class MqttPacketSerialization_V5_Tests
SubscriptionIdentifiers = [123],
MessageExpiryInterval = 38,
PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData,
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V500);
@@ -284,7 +285,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubRecReasonCode.UnspecifiedError,
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V500);
@@ -303,7 +304,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound,
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V500);
@@ -322,7 +323,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonString = "ReasonString",
ReasonCodes = [MqttSubscribeReasonCode.GrantedQoS1],
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V500);
@@ -352,7 +353,7 @@ public sealed class MqttPacketSerialization_V5_Tests
QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce
}
],
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V500);
@@ -376,7 +377,7 @@ public sealed class MqttPacketSerialization_V5_Tests
PacketIdentifier = 123,
ReasonCodes = [MqttUnsubscribeReasonCode.ImplementationSpecificError],
ReasonString = "ReasonString",
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V500);
@@ -395,7 +396,7 @@ public sealed class MqttPacketSerialization_V5_Tests
{
PacketIdentifier = 123,
TopicFilters = ["TopicFilter1"],
UserProperties = [new MqttUserProperty("Foo", "Bar")]
UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))]
};
var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V500);
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Packets;
namespace MQTTnet.Tests.Formatter;
[TestClass]
public sealed class MqttV5PublishPropertyValidation_Tests
{
[TestMethod]
[DataRow("0100")]
[DataRow("0200000000")]
[DataRow("030000")]
[DataRow("08000161")]
[DataRow("090000")]
[DataRow("230001")]
public void Publish_Rejects_Duplicate_Singleton_Property(string propertyHex)
{
var property = Convert.FromHexString(propertyHex);
byte[] packet = [0x30, (byte)(4 + 2 * property.Length), 0, 1, (byte)'a', (byte)(2 * property.Length), .. property, .. property];
Assert.ThrowsExactly<MqttProtocolViolationException>(() => MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500));
}
[TestMethod]
public void Publish_Allows_Repeated_Subscription_Identifiers_And_User_Properties()
{
byte[] packet = [0x30, 18, 0, 1, (byte)'a', 14, 0x0B, 1, 0x26, 0, 0, 0, 0, 0x0B, 1, 0x26, 0, 0, 0, 0];
var decoded = (MqttPublishPacket)MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500);
CollectionAssert.AreEqual(new uint[] { 1, 1 }, decoded.SubscriptionIdentifiers);
Assert.HasCount(2, decoded.UserProperties);
}
}
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Adapter;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Formatter.V5;
using MQTTnet.Packets;
namespace MQTTnet.Tests.Formatter;
[TestClass]
public sealed class MqttV5SubscribeIdentifier_Tests
{
[TestMethod]
[DataRow("0B00")]
[DataRow("0B010B01")]
[DataRow("0B010B02")]
[DataRow("0B0126000000000B02")]
public void Reject_Invalid_Subscription_Identifier(string propertiesHex)
{
var properties = Convert.FromHexString(propertiesHex);
byte[] packet = [0x82, (byte)(7 + properties.Length), 0, 1, (byte)properties.Length, .. properties, 0, 1, (byte)'a', 0];
Assert.ThrowsExactly<MqttProtocolViolationException>(() => MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500));
}
[TestMethod]
[DataRow("", 0U)]
[DataRow("0B01", 1U)]
[DataRow("0BFFFFFF7F", 268435455U)]
[DataRow("26000000000B012600000000", 1U)]
public void Accept_Valid_Subscription_Identifier(string propertiesHex, uint expected)
{
var properties = Convert.FromHexString(propertiesHex);
byte[] packet = [0x82, (byte)(7 + properties.Length), 0, 1, (byte)properties.Length, .. properties, 0, 1, (byte)'a', 0];
var decoded = (MqttSubscribePacket)MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500);
Assert.AreEqual(expected, decoded.SubscriptionIdentifier);
}
[TestMethod]
public void Accept_Same_Identifier_In_Separate_Subscribe_Packets()
{
byte[] packet = [0x82, 9, 0, 1, 2, 0x0B, 1, 0, 1, (byte)'a', 0];
var decoder = new MqttV5PacketDecoder();
var received = new ReceivedMqttPacket(packet[0], new ArraySegment<byte>(packet, 2, packet.Length - 2), packet.Length);
Assert.AreEqual(1U, ((MqttSubscribePacket)decoder.Decode(received)).SubscriptionIdentifier);
Assert.AreEqual(1U, ((MqttSubscribePacket)decoder.Decode(received)).SubscriptionIdentifier);
}
[TestMethod]
public void Accept_Repeated_Subscription_Identifiers_In_Publish()
{
byte[] packet = [0x30, 8, 0, 1, (byte)'a', 4, 0x0B, 1, 0x0B, 1];
var decoded = (MqttPublishPacket)MqttPacketSerializationHelper.DecodePacket(packet, MqttProtocolVersion.V500);
CollectionAssert.AreEqual(new uint[] { 1, 1 }, decoded.SubscriptionIdentifiers);
}
}
@@ -2,8 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Server;
using MQTTnet.Server.Exceptions;
namespace MQTTnet.Tests.Internal;
@@ -11,6 +14,57 @@ namespace MQTTnet.Tests.Internal;
[TestClass]
public sealed class MqttPacketBusItem_Tests
{
[TestMethod]
public void Fail_Without_Waiter_Does_Not_Raise_Unobserved_Exception()
{
var exception = new MqttPendingMessagesOverflowException("test", MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage);
var unobservedExceptions = 0;
void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
if (args.Exception.Flatten().InnerExceptions.Contains(exception))
{
Interlocked.Increment(ref unobservedExceptions);
args.SetObserved();
}
}
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
try
{
var taskReference = CreateFailedPacketBusItem(exception);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.IsFalse(taskReference.IsAlive, "The faulted task must be collected to exercise unobserved exception reporting.");
Assert.AreEqual(0, unobservedExceptions);
}
finally
{
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
}
}
[TestMethod]
[DataRow(false)]
[DataRow(true)]
public async Task Wait_Failed_Packet_Bus_Item_Preserves_Exception(bool waitBeforeFailure)
{
var item = new MqttPacketBusItem(new MqttPublishPacket());
var exception = new MqttPendingMessagesOverflowException("test", MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage);
var task = waitBeforeFailure ? item.WaitAsync() : null;
item.Fail(exception);
task ??= item.WaitAsync();
var thrownException = await Assert.ThrowsExactlyAsync<MqttPendingMessagesOverflowException>(async () => await task);
Assert.AreSame(exception, thrownException);
Assert.IsTrue(task.IsFaulted);
}
[TestMethod]
public void Fire_Completed_Event()
{
@@ -40,4 +94,13 @@ public sealed class MqttPacketBusItem_Tests
await item.WaitAsync();
});
}
[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference CreateFailedPacketBusItem(Exception exception)
{
// Keep strong references out of the collecting method, including in debug builds.
var item = new MqttPacketBusItem(new MqttPublishPacket());
item.Fail(exception);
return new WeakReference(item.WaitAsync());
}
}
+8 -4
View File
@@ -1,19 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<LangVersion>default</LangVersion>
<Nullable>disable</Nullable>
<NoWarn>$(NoWarn);CA1707</NoWarn>
<EnableMSTestRunner>true</EnableMSTestRunner>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="MSTest" Version="4.0.1" />
<PackageReference Include="MSTest" Version="4.0.2" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
@@ -25,4 +23,10 @@
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
</ItemGroup>
<ItemGroup>
<None Include=".\..\..\global.json"
Link="global.json"
CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
</Project>
+11 -10
View File
@@ -4,6 +4,7 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.IO;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -64,8 +65,8 @@ public sealed class Client_Tests : BaseTestClass
await client.PublishAsync(
new MqttApplicationMessageBuilder().WithTopic("a")
.WithPayload("x")
.WithUserProperty("a", "1")
.WithUserProperty("b", "2")
.WithUserProperty("a", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("b", Encoding.UTF8.GetBytes("2"))
.WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData)
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
@@ -98,8 +99,8 @@ public sealed class Client_Tests : BaseTestClass
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello")
.WithPayload("World")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
.WithUserProperty("x", "1")
.WithUserProperty("y", "2")
.WithUserProperty("x", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("y", Encoding.UTF8.GetBytes("2"))
.WithResponseTopic("response")
.WithContentType("text")
.WithMessageExpiryInterval(50)
@@ -113,7 +114,7 @@ public sealed class Client_Tests : BaseTestClass
Assert.IsNotNull(receivedMessage);
Assert.AreEqual(applicationMessage.Topic, receivedMessage.Topic);
Assert.AreEqual(applicationMessage.TopicAlias, receivedMessage.TopicAlias);
Assert.AreEqual(0, receivedMessage.TopicAlias);
Assert.AreEqual(applicationMessage.ContentType, receivedMessage.ContentType);
Assert.AreEqual(applicationMessage.ResponseTopic, receivedMessage.ResponseTopic);
Assert.AreEqual(applicationMessage.MessageExpiryInterval, receivedMessage.MessageExpiryInterval);
@@ -214,8 +215,8 @@ public sealed class Client_Tests : BaseTestClass
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello")
.WithPayload("World")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
.WithUserProperty("x", "1")
.WithUserProperty("y", "2")
.WithUserProperty("x", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("y", Encoding.UTF8.GetBytes("2"))
.WithResponseTopic("response")
.WithContentType("text")
.WithMessageExpiryInterval(50)
@@ -250,8 +251,8 @@ public sealed class Client_Tests : BaseTestClass
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello")
.WithPayload(memoryStream.GetReadOnlySequence())
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
.WithUserProperty("x", "1")
.WithUserProperty("y", "2")
.WithUserProperty("x", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("y", Encoding.UTF8.GetBytes("2"))
.WithResponseTopic("response")
.WithContentType("text")
.WithMessageExpiryInterval(50)
@@ -343,4 +344,4 @@ public sealed class Client_Tests : BaseTestClass
memoryStream.Position = 0;
return memoryStream;
}
}
}
@@ -2,7 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text;
using MQTTnet.Formatter;
using MQTTnet.Formatter.V5;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Tests;
@@ -59,4 +64,64 @@ public sealed class MqttApplicationMessageBuilder_Tests
Assert.IsTrue(message.Retain);
Assert.AreEqual(MqttQualityOfServiceLevel.ExactlyOnce, message.QualityOfServiceLevel);
}
[TestMethod]
public void CreateApplicationMessage_UserProperty_ReadOnlyMemoryValue()
{
var value = "utf8";
var buffer = Encoding.UTF8.GetBytes(value);
var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", buffer.AsMemory()).Build();
Assert.IsNotNull(message.UserProperties);
Assert.HasCount(1, message.UserProperties);
var userProperty = message.UserProperties[0];
CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray());
Assert.AreEqual(value, userProperty.ReadValueAsString());
}
[TestMethod]
public void CreateApplicationMessage_UserProperty_ArraySegmentValue()
{
var buffer = Encoding.UTF8.GetBytes("segment");
var segment = new ArraySegment<byte>(buffer);
var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", segment).Build();
Assert.IsNotNull(message.UserProperties);
Assert.HasCount(1, message.UserProperties);
var userProperty = message.UserProperties[0];
CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray());
Assert.AreEqual("segment", userProperty.ReadValueAsString());
}
[TestMethod]
public void WriteUserProperty_FromBinaryBuffer_EqualsStringEncoding()
{
var name = "name";
var value = "value";
var encoded = Encoding.UTF8.GetBytes(value);
var binaryWriter = new MqttBufferWriter(32, 256);
var binaryPropertiesWriter = new MqttV5PropertiesWriter(binaryWriter);
binaryPropertiesWriter.WriteUserProperties(new List<MqttUserProperty> { new(name, encoded.AsMemory()) });
var stringWriter = new MqttBufferWriter(32, 256);
var stringPropertiesWriter = new MqttV5PropertiesWriter(stringWriter);
#pragma warning disable CS0618 // Type or member is obsolete
stringPropertiesWriter.WriteUserProperties(new List<MqttUserProperty> { new(name, value) });
#pragma warning restore CS0618 // Type or member is obsolete
CollectionAssert.AreEqual(GetWrittenBytes(stringWriter), GetWrittenBytes(binaryWriter));
}
static byte[] GetWrittenBytes(MqttBufferWriter writer)
{
var length = writer.Length;
var copy = new byte[length];
Array.Copy(writer.GetBuffer(), copy, length);
return copy;
}
}
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Formatter;
namespace MQTTnet.Tests;
@@ -27,7 +28,7 @@ public sealed class MqttApplicationMessageValidator_Tests
public void Succeed_When_Using_UserProperties_And_MQTT_500()
{
MqttApplicationMessageValidator.ThrowIfNotSupported(
new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", "Property").Build(),
new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(),
MqttProtocolVersion.V500);
}
@@ -35,7 +36,7 @@ public sealed class MqttApplicationMessageValidator_Tests
public void Succeed_When_Using_WillUserProperties_And_MQTT_311()
{
Assert.ThrowsExactly<NotSupportedException>(() => MqttApplicationMessageValidator.ThrowIfNotSupported(
new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(),
new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(),
MqttProtocolVersion.V311));
}
}
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Formatter;
namespace MQTTnet.Tests;
@@ -13,27 +14,27 @@ public sealed class MqttClientOptionsValidator_Tests
[TestMethod]
public void Succeed_When_Using_UserProperties_And_MQTT_500()
{
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build();
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build();
}
[TestMethod]
public void Succeed_When_Using_WillUserProperties_And_MQTT_311()
{
Assert.ThrowsExactly<NotSupportedException>(() =>
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build());
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build());
}
[TestMethod]
public void Succeed_When_Using_WillUserProperties_And_MQTT_500()
{
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build();
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithWillUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build();
}
[TestMethod]
public void Throw_When_Using_UserProperties_And_MQTT_311()
{
Assert.ThrowsExactly<NotSupportedException>(() =>
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build());
new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build());
}
[TestMethod]
+30 -3
View File
@@ -3,7 +3,10 @@
// See the LICENSE file in the project root for more information.
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using MQTTnet.Implementations;
namespace MQTTnet.Tests;
@@ -12,6 +15,21 @@ namespace MQTTnet.Tests;
[TestClass]
public class MqttTcpChannel_Tests
{
[TestMethod]
public void Certificate_Validation_Uses_Default_Handler_When_No_Custom_Handler()
{
var options = new MqttClientOptionsBuilder().WithTcpServer("localhost")
.WithTlsOptions(o => o.WithAllowUntrustedCertificates().WithIgnoreCertificateRevocationErrors())
.Build();
var tcpChannel = new MqttTcpChannel(options);
using var chain = new X509Chain();
var isValid = InvokeCertificateValidationCallback(tcpChannel, chain, SslPolicyErrors.RemoteCertificateChainErrors);
Assert.IsTrue(isValid);
}
[TestMethod]
public async Task Dispose_Channel_While_Used()
{
@@ -20,7 +38,8 @@ public class MqttTcpChannel_Tests
try
{
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
serverSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
var serverPort = ((IPEndPoint)serverSocket.LocalEndPoint).Port;
serverSocket.Listen(0);
#pragma warning disable 4014
@@ -37,7 +56,7 @@ public class MqttTcpChannel_Tests
},
ct.Token);
var remoteEndPoint = new DnsEndPoint("localhost", 50001);
var remoteEndPoint = new DnsEndPoint("localhost", serverPort);
using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None);
@@ -76,4 +95,12 @@ public class MqttTcpChannel_Tests
ct.Cancel(false);
}
}
}
static bool InvokeCertificateValidationCallback(MqttTcpChannel tcpChannel, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var method = typeof(MqttTcpChannel).GetMethod("InternalUserCertificateValidationCallback", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(method);
return (bool)method.Invoke(tcpChannel, [null, null, chain, sslPolicyErrors]);
}
}
@@ -43,7 +43,7 @@ public sealed class Cross_Version_Tests : BaseTestClass
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message")
.WithPayload("My_Payload")
.WithUserProperty("A", "B")
.WithUserProperty("A", Encoding.UTF8.GetBytes("B"))
.WithResponseTopic("Response")
.WithCorrelationData(Encoding.UTF8.GetBytes("Correlation"))
.Build();
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -26,6 +27,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
@@ -47,6 +49,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
@@ -70,6 +73,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
await client01.Connect();
@@ -91,6 +95,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
await client01.Connect();
@@ -122,10 +127,12 @@ public sealed class HotSwapCerts_Tests
certRequest.CertificateExtensions.Add(sanBuilder.Build());
using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10));
#pragma warning disable SYSLIB0057
var pfxCertificate = new X509Certificate2(
certificate.Export(X509ContentType.Pfx),
(string)null,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
#pragma warning restore SYSLIB0057
return pfxCertificate;
}
@@ -133,10 +140,11 @@ public sealed class HotSwapCerts_Tests
sealed class ClientTestHarness : IDisposable
{
readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider();
int _serverPort = 8883;
IMqttClient _client;
public string ClientId => _client.Options.ClientId;
public string ClientId => _client?.Options?.ClientId;
public Task Connect()
{
@@ -145,10 +153,15 @@ public sealed class HotSwapCerts_Tests
public void Dispose()
{
_client.Dispose();
_client?.Dispose();
_hotSwapClient.Dispose();
}
public void SetServerPort(int port)
{
_serverPort = port;
}
public X509Certificate2 GetCurrentClientCert()
{
var result = _hotSwapClient.GetCertificates()[0];
@@ -214,7 +227,7 @@ public sealed class HotSwapCerts_Tests
o => o.WithClientCertificatesProvider(_hotSwapClient)
.WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation)
.WithSslProtocols(SslProtocols.Tls12))
.WithTcpServer("localhost")
.WithTcpServer("localhost", _serverPort)
.WithCleanSession()
.WithProtocolVersion(MqttProtocolVersion.V500);
@@ -240,6 +253,7 @@ public sealed class HotSwapCerts_Tests
sealed class ServerTestHarness : IDisposable
{
readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider();
readonly int _encryptedPort = GetFreeTcpPort();
MqttServer _server;
@@ -259,6 +273,8 @@ public sealed class HotSwapCerts_Tests
return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError);
}
public int EncryptedPort => _encryptedPort;
public X509Certificate2 GetCurrentServerCert()
{
return _hotSwapServer.GetCertificate();
@@ -279,6 +295,7 @@ public sealed class HotSwapCerts_Tests
var mqttServerFactory = new MqttServerFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer)
.WithEncryptedEndpointPort(_encryptedPort)
.WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback)
.WithEncryptedEndpoint()
.Build();
@@ -288,6 +305,17 @@ public sealed class HotSwapCerts_Tests
_server = mqttServerFactory.CreateMqttServer(mqttServerOptions);
return _server.StartAsync();
}
static int GetFreeTcpPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
sealed class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable
@@ -48,7 +48,7 @@ public sealed class Injection_Tests : BaseTestClass
[TestMethod]
public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy()
{
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
using var testEnvironment = CreateTestEnvironment();
var server = await testEnvironment.StartServer(
builder => builder
@@ -126,7 +126,7 @@ public sealed class Injection_Tests : BaseTestClass
[TestMethod]
public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy()
{
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
using var testEnvironment = CreateTestEnvironment();
var server = await testEnvironment.StartServer(
builder => builder
@@ -322,7 +322,7 @@ public sealed class Injection_Tests : BaseTestClass
[TestMethod]
public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy()
{
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
using var testEnvironment = CreateTestEnvironment();
var server = await testEnvironment.StartServer(
builder => builder
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -98,7 +99,7 @@ public sealed class Subscribe_Tests : BaseTestClass
var client = await testEnvironment.ConnectClient();
var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build();
await client.SubscribeAsync(subscribeOptions);
CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Linq;
using System.Text;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -520,10 +521,11 @@ public sealed class SubscriptionTopicHashTests : IDisposable
static byte[] GetBytes(ulong value)
{
var bytes = BitConverter.GetBytes(value);
// Ensure that highest byte comes first for comparison left to right
if (BitConverter.IsLittleEndian)
{
return bytes.Reverse().ToArray();
bytes = bytes.AsEnumerable().Reverse().ToArray();
}
return bytes;
+2
View File
@@ -31,10 +31,12 @@ public sealed class Tls_Tests : BaseTestClass
certRequest.CertificateExtensions.Add(sanBuilder.Build());
using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10));
#pragma warning disable SYSLIB0057
var pfxCertificate = new X509Certificate2(
certificate.Export(X509ContentType.Pfx),
(string)null,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
#pragma warning restore SYSLIB0057
return pfxCertificate;
}
@@ -34,6 +34,7 @@ public sealed class Topic_Alias_Tests : BaseTestClass
await testEnvironment.StartServer();
var receivedTopics = new List<string>();
var receivedTopicAliases = new List<ushort>();
var c1 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500));
c1.ApplicationMessageReceivedAsync += e =>
@@ -41,6 +42,7 @@ public sealed class Topic_Alias_Tests : BaseTestClass
lock (receivedTopics)
{
receivedTopics.Add(e.ApplicationMessage.Topic);
receivedTopicAliases.Add(e.ApplicationMessage.TopicAlias);
}
return CompletedTask.Instance;
@@ -68,5 +70,6 @@ public sealed class Topic_Alias_Tests : BaseTestClass
Assert.HasCount(3, receivedTopics);
CollectionAssert.AllItemsAreNotNull(receivedTopics);
Assert.IsTrue(receivedTopics.All(t => t.Equals("this_is_the_topic", System.StringComparison.Ordinal)));
Assert.IsTrue(receivedTopicAliases.All(a => a == 0));
}
}
}
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -45,7 +46,7 @@ public sealed class Unsubscribe_Tests : BaseTestClass
var client = await testEnvironment.ConnectClient();
var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build();
await client.UnsubscribeAsync(unsubscribeOptions);
CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
@@ -26,10 +27,10 @@ public class Feature_Tests
var message = new MqttApplicationMessageBuilder()
.WithTopic("A")
.WithUserProperty("x", "1")
.WithUserProperty("y", "2")
.WithUserProperty("z", "3")
.WithUserProperty("z", "4"); // z is here two times to test list of items
.WithUserProperty("x", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("y", Encoding.UTF8.GetBytes("2"))
.WithUserProperty("z", Encoding.UTF8.GetBytes("3"))
.WithUserProperty("z", Encoding.UTF8.GetBytes("4")); // z is here two times to test list of items
await receiver.SubscribeAsync(new MqttClientSubscribeOptions
{
@@ -14,6 +14,17 @@ public class BlobCertificateProvider(byte[] blob) : ICertificateProvider
public X509Certificate2 GetCertificate()
{
#if NET10_0_OR_GREATER
if (string.IsNullOrEmpty(Password))
{
return X509CertificateLoader.LoadCertificate(Blob);
}
return X509CertificateLoader.LoadPkcs12(Blob, Password);
#else
if (string.IsNullOrEmpty(Password))
{
// Use a different overload when no password is specified. Otherwise, the constructor will fail.
@@ -21,5 +32,7 @@ public class BlobCertificateProvider(byte[] blob) : ICertificateProvider
}
return new X509Certificate2(Blob, Password);
#endif
}
}
@@ -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.Net;
namespace MQTTnet.Channel;
/// <summary>
/// Optional marker interface that a <see cref="Stream"/> returned by an
/// <see cref="IMqttClientStreamProvider"/> may implement to expose the local and remote
/// socket endpoints to the MQTT TCP channel (used for diagnostics only).
/// </summary>
public interface IMqttClientStreamEndPoints
{
/// <summary>
/// Gets the local endpoint of the socket actually opened by the provider
/// (e.g. the local interface used to reach the proxy). May be <c>null</c> if not known.
/// </summary>
EndPoint LocalEndPoint { get; }
/// <summary>
/// Gets the remote endpoint of the socket actually opened by the provider
/// (e.g. the proxy address). May be <c>null</c> if not known.
/// </summary>
EndPoint RemoteEndPoint { get; }
}
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Net;
namespace MQTTnet.Channel;
/// <summary>
/// Produces a connected byte <see cref="Stream"/> that carries traffic to an MQTT broker endpoint.
/// This is the extension point used to plug stream-level proxies (SOCKS5, HTTP CONNECT, etc.)
/// into the TCP transport without modifying <c>MqttTcpChannel</c>.
/// Implementations are responsible for the full handshake to the broker (proxy negotiation,
/// authentication, target connect) but MUST NOT perform any TLS wrapping; TLS is layered on
/// top of the returned stream by the channel itself, so the broker host can be used as SNI.
/// </summary>
/// <remarks>
/// <para>
/// The returned <see cref="Stream"/> is owned by the caller (the MQTT channel). Disposing
/// it will release any underlying socket. Providers should therefore avoid keeping their
/// own references to the underlying socket after returning.
/// </para>
/// <para>
/// Implementations may optionally have the returned stream implement
/// <see cref="IMqttClientStreamEndPoints"/> to expose the local and remote socket
/// endpoints to <c>MqttTcpChannel</c> for diagnostics.
/// </para>
/// <para>
/// <see cref="ConnectAsync"/> is invoked on every (re)connect attempt; implementations
/// must be safe to call concurrently or sequentially across multiple client instances.
/// </para>
/// </remarks>
public interface IMqttClientStreamProvider
{
/// <summary>
/// Establishes a connection to the MQTT broker <paramref name="brokerEndPoint"/>, performing
/// any required proxy handshake, and returns the resulting raw byte stream.
/// </summary>
/// <param name="brokerEndPoint">
/// The MQTT broker endpoint. Can be a <see cref="DnsEndPoint"/> (in which case the
/// provider may forward the hostname for remote DNS resolution) or an
/// <see cref="IPEndPoint"/>.
/// </param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>
/// A connected <see cref="Stream"/> whose bytes are transported end-to-end to
/// <paramref name="brokerEndPoint"/>.
/// </returns>
Task<Stream> ConnectAsync(EndPoint brokerEndPoint, CancellationToken cancellationToken);
}
@@ -63,7 +63,7 @@ public static class MqttClientConnectResultFactory
RetainAvailable = connAckPacket.RetainAvailable,
AssignedClientIdentifier = connAckPacket.AssignedClientIdentifier,
AuthenticationMethod = connAckPacket.AuthenticationMethod,
AuthenticationData = connAckPacket.AuthenticationData.ToArray(),
AuthenticationData = connAckPacket.AuthenticationData,
MaximumPacketSize = connAckPacket.MaximumPacketSize,
ReasonString = connAckPacket.ReasonString,
ReceiveMaximum = connAckPacket.ReceiveMaximum,
@@ -2,6 +2,7 @@
// The .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;
@@ -48,10 +49,25 @@ public sealed class MqttClientDisconnectOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Exceptions;
/// <summary>
/// Categorized error codes for proxy-level failures surfaced via <see cref="MqttProxyException"/>.
/// The first set of values map 1:1 to SOCKS5 REP codes (RFC 1928 §6); additional values cover
/// non-protocol conditions (connecting to the proxy itself, authentication, protocol violations).
/// </summary>
public enum MqttProxyErrorCode
{
/// <summary>The error category could not be classified.</summary>
ProxyUnknownError = 0,
/// <summary>The TCP connection to the proxy could not be established (DNS, refused, timeout).</summary>
ProxyUnreachable,
/// <summary>The proxy returned a response that does not conform to the expected protocol.</summary>
ProxyProtocolError,
/// <summary>The proxy rejected every authentication method offered by the client (SOCKS5 method = 0xFF).</summary>
ProxyAuthMethodRejected,
/// <summary>The proxy rejected the supplied credentials.</summary>
ProxyAuthFailed,
/// <summary>SOCKS5 REP = 0x01: general SOCKS server failure.</summary>
ProxyGeneralFailure,
/// <summary>SOCKS5 REP = 0x02: connection not allowed by ruleset.</summary>
ProxyConnectionNotAllowed,
/// <summary>SOCKS5 REP = 0x03: network unreachable.</summary>
ProxyNetworkUnreachable,
/// <summary>SOCKS5 REP = 0x04: host unreachable.</summary>
ProxyHostUnreachable,
/// <summary>SOCKS5 REP = 0x05: connection refused by destination.</summary>
ProxyConnectionRefused,
/// <summary>SOCKS5 REP = 0x06: TTL expired.</summary>
ProxyTtlExpired,
/// <summary>SOCKS5 REP = 0x07: command not supported by the proxy.</summary>
ProxyCommandNotSupported,
/// <summary>SOCKS5 REP = 0x08: address type not supported by the proxy.</summary>
ProxyAddressTypeNotSupported
}
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MQTTnet.Exceptions;
/// <summary>
/// Thrown when an <see cref="MQTTnet.Channel.IMqttClientStreamProvider"/> fails while
/// establishing the stream to the broker (proxy unreachable, authentication failure,
/// destination unreachable, protocol violation, ...).
/// Inherits from <see cref="MqttCommunicationException"/> so existing connect/reconnect
/// handling continues to work; callers that care about the specific reason can branch on
/// <see cref="ErrorCode"/>.
/// </summary>
public sealed class MqttProxyException : MqttCommunicationException
{
public MqttProxyException(MqttProxyErrorCode errorCode, string message, string proxyAddress = null, Exception innerException = null)
: base(BuildMessage(errorCode, message, proxyAddress), innerException)
{
ErrorCode = errorCode;
ProxyAddress = proxyAddress;
}
/// <summary>Gets the categorized error reason.</summary>
public MqttProxyErrorCode ErrorCode { get; }
/// <summary>Gets the proxy address (host:port) associated with the failure, if known.</summary>
public string ProxyAddress { get; }
static string BuildMessage(MqttProxyErrorCode errorCode, string message, string proxyAddress)
{
if (string.IsNullOrEmpty(proxyAddress))
{
return $"Proxy error '{errorCode}': {message}";
}
return $"Proxy '{proxyAddress}' error '{errorCode}': {message}";
}
}
@@ -21,7 +21,7 @@ public static class MqttApplicationMessageFactory
Dup = publishPacket.Dup,
ResponseTopic = publishPacket.ResponseTopic,
ContentType = publishPacket.ContentType,
CorrelationData = publishPacket.CorrelationData.ToArray(),
CorrelationData = publishPacket.CorrelationData,
MessageExpiryInterval = publishPacket.MessageExpiryInterval,
SubscriptionIdentifiers = publishPacket.SubscriptionIdentifiers,
TopicAlias = publishPacket.TopicAlias,
+1 -1
View File
@@ -58,7 +58,7 @@ public sealed class MqttBufferReader
return value;
}
public ReadOnlyMemory<byte> ReadRemainingData()
public byte[] ReadRemainingData()
{
var bufferLength = BytesLeft;
if (bufferLength == 0)
+60 -82
View File
@@ -2,8 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using System.Buffers.Binary;
using System;
using System.Runtime.CompilerServices;
using System.Text;
using MQTTnet.Exceptions;
@@ -110,47 +109,31 @@ public sealed class MqttBufferWriter
WriteBinary(propertyWriter._buffer, 0, propertyWriter.Length);
}
public void WriteBinary(ReadOnlySequence<byte> value)
public void WriteBinary(byte[] value)
{
if (value.IsEmpty)
if (value == null || value.Length == 0)
{
WriteEmptyBinary();
return;
EnsureAdditionalCapacity(2);
_buffer[_position] = 0;
_buffer[_position + 1] = 0;
IncreasePosition(2);
}
EnsureAdditionalCapacity(value.Length + 2);
_buffer[_position] = (byte)(value.Length >> 8);
_buffer[_position + 1] = (byte)value.Length;
Advance(2);
foreach (var segment in value)
else
{
MqttMemoryHelper.Copy(segment, 0, _buffer, _position + 2, segment.Length);
Advance(segment.Length);
var valueLength = value.Length;
EnsureAdditionalCapacity(valueLength + 2);
_buffer[_position] = (byte)(valueLength >> 8);
_buffer[_position + 1] = (byte)valueLength;
MqttMemoryHelper.Copy(value, 0, _buffer, _position + 2, valueLength);
IncreasePosition(valueLength + 2);
}
}
public void WriteBinary(ReadOnlyMemory<byte> value)
{
if (value.Length == 0)
{
WriteEmptyBinary();
return;
}
EnsureAdditionalCapacity(value.Length + 2);
_buffer[_position] = (byte)(value.Length >> 8);
_buffer[_position + 1] = (byte)value.Length;
Advance(2);
MqttMemoryHelper.Copy(value, 0, _buffer, _position + 2, (int)value.Length);
Advance(value.Length);
}
public void WriteBinary(byte[] buffer, int offset, int count)
{
ArgumentNullException.ThrowIfNull(buffer);
@@ -163,7 +146,15 @@ public sealed class MqttBufferWriter
EnsureAdditionalCapacity(count);
MqttMemoryHelper.Copy(buffer, offset, _buffer, _position, count);
Advance(count);
IncreasePosition(count);
}
public void WriteByte(byte @byte)
{
EnsureAdditionalCapacity(1);
_buffer[_position] = @byte;
IncreasePosition(1);
}
public void WriteString(string value)
@@ -175,7 +166,7 @@ public sealed class MqttBufferWriter
_buffer[_position] = 0;
_buffer[_position + 1] = 0;
Advance(2);
IncreasePosition(2);
}
else
{
@@ -199,52 +190,49 @@ public sealed class MqttBufferWriter
_buffer[_position] = (byte)(writtenBytes >> 8);
_buffer[_position + 1] = (byte)writtenBytes;
Advance(writtenBytes + 2);
IncreasePosition(writtenBytes + 2);
}
}
public void WriteByte(byte @byte)
public void WriteString(ReadOnlyMemory<byte> value)
{
const int size = sizeof(byte);
var span = GetSpan(size);
var span = value.Span;
var length = span.Length;
span[0] = @byte;
Advance(size);
if (length > EncodedStringMaxLength)
{
throw new MqttProtocolViolationException($"The maximum string length is 65535. The current string has a length of {length}.");
}
EnsureAdditionalCapacity(length + 2);
_buffer[_position] = (byte)(length >> 8);
_buffer[_position + 1] = (byte)length;
if (length > 0)
{
span.CopyTo(_buffer.AsSpan(_position + 2));
}
IncreasePosition(length + 2);
}
public void WriteTwoByteInteger(ushort value)
{
const int size = sizeof(ushort);
var span = GetSpan(size);
EnsureAdditionalCapacity(2);
BinaryPrimitives.WriteUInt16BigEndian(span, value);
Advance(size);
_buffer[_position] = (byte)(value >> 8);
IncreasePosition(1);
_buffer[_position] = (byte)value;
IncreasePosition(1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Span<byte> GetSpan(int size)
{
var freeSpace = _buffer.Length - _position;
if (freeSpace < size)
{
EnsureCapacity(_buffer.Length + size - freeSpace);
}
return _buffer.AsSpan(_position, size);
}
public void WriteVariableByteInteger(uint value)
{
if (value == 0)
{
_buffer[_position] = 0;
Advance(1);
IncreasePosition(1);
return;
}
@@ -252,7 +240,7 @@ public sealed class MqttBufferWriter
if (value <= 127)
{
_buffer[_position] = (byte)value;
Advance(1);
IncreasePosition(1);
return;
}
@@ -274,11 +262,11 @@ public sealed class MqttBufferWriter
size++;
} while (x > 0);
Advance(size);
IncreasePosition(size);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void EnsureAdditionalCapacity(long additionalCapacity)
void EnsureAdditionalCapacity(int additionalCapacity)
{
var bufferLength = _buffer.Length;
@@ -292,7 +280,7 @@ public sealed class MqttBufferWriter
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void EnsureCapacity(long capacity)
void EnsureCapacity(int capacity)
{
var newBufferLength = _buffer.Length;
@@ -311,7 +299,7 @@ public sealed class MqttBufferWriter
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void Advance(int length)
void IncreasePosition(int length)
{
_position += length;
@@ -322,14 +310,4 @@ public sealed class MqttBufferWriter
Length = _position;
}
}
void WriteEmptyBinary()
{
EnsureAdditionalCapacity(2);
_buffer[_position] = 0;
_buffer[_position + 1] = 0;
Advance(2);
}
}
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using MQTTnet.Packets;
namespace MQTTnet.Formatter;
@@ -41,7 +40,7 @@ public static class MqttConnectPacketFactory
connectPacket.WillFlag = true;
connectPacket.WillTopic = clientOptions.WillTopic;
connectPacket.WillQoS = clientOptions.WillQualityOfServiceLevel;
connectPacket.WillMessage = new ReadOnlySequence<byte>(clientOptions.WillPayload);
connectPacket.WillMessage = clientOptions.WillPayload;
connectPacket.WillRetain = clientOptions.WillRetain;
connectPacket.WillDelayInterval = clientOptions.WillDelayInterval;
connectPacket.WillContentType = clientOptions.WillContentType;
@@ -201,7 +201,7 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
packet.WillRetain = willRetain;
packet.WillTopic = _bufferReader.ReadString();
packet.WillMessage = new ReadOnlySequence<byte>(_bufferReader.ReadBinaryData());
packet.WillMessage = _bufferReader.ReadBinaryData();
}
if (usernameFlag)
@@ -271,7 +271,7 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
if (!_bufferReader.EndOfStream)
{
packet.Payload = new ReadOnlySequence<byte>(_bufferReader.ReadRemainingData());
packet.PayloadSegment = new ArraySegment<byte>(_bufferReader.ReadRemainingData());
}
return packet;
@@ -430,12 +430,12 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
}
}
if (packet.Password.Length > 0 && packet.Username == null)
if (packet.Password != null && packet.Username == null)
{
throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22].");
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
connectFlags |= 0x40;
}
@@ -460,7 +460,7 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
bufferWriter.WriteString(packet.Username);
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
bufferWriter.WriteBinary(packet.Password);
}
@@ -501,12 +501,12 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
}
}
if (packet.Password.Length > 0 && packet.Username == null)
if (packet.Password != null && packet.Username == null)
{
throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22].");
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
connectFlags |= 0x40;
}
@@ -531,7 +531,7 @@ public sealed class MqttV3PacketFormatter : IMqttPacketFormatter
bufferWriter.WriteString(packet.Username);
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
bufferWriter.WriteBinary(packet.Password);
}
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using MQTTnet.Adapter;
using MQTTnet.Exceptions;
using MQTTnet.Packets;
@@ -315,7 +314,7 @@ public sealed class MqttV5PacketDecoder
}
packet.WillTopic = _bufferReader.ReadString();
packet.WillMessage = new ReadOnlySequence<byte>(_bufferReader.ReadBinaryData());
packet.WillMessage = _bufferReader.ReadBinaryData();
packet.WillUserProperties = willPropertiesReader.CollectedUserProperties;
}
@@ -477,8 +476,28 @@ public sealed class MqttV5PacketDecoder
}
var propertiesReader = new MqttV5PropertiesReader(_bufferReader);
var seenSingletonProperties = 0;
while (propertiesReader.MoveNext())
{
// MQTT 5.0 section 3.3.2.3 permits only User Properties and
// Subscription Identifiers to occur more than once in PUBLISH.
var propertyFlag = propertiesReader.CurrentPropertyId switch
{
MqttPropertyId.PayloadFormatIndicator => 1,
MqttPropertyId.MessageExpiryInterval => 2,
MqttPropertyId.ContentType => 4,
MqttPropertyId.ResponseTopic => 8,
MqttPropertyId.CorrelationData => 16,
MqttPropertyId.TopicAlias => 32,
_ => 0
};
if ((seenSingletonProperties & propertyFlag) != 0)
{
throw new MqttProtocolViolationException($"Property '{propertiesReader.CurrentPropertyId}' must not occur more than once in a PUBLISH packet.");
}
seenSingletonProperties |= propertyFlag;
if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator)
{
packet.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator();
@@ -522,7 +541,7 @@ public sealed class MqttV5PacketDecoder
if (!_bufferReader.EndOfStream)
{
packet.Payload = new ReadOnlySequence<byte>(_bufferReader.ReadBinaryData());
packet.PayloadSegment = new ArraySegment<byte>(_bufferReader.ReadRemainingData());
}
return packet;
@@ -654,7 +673,17 @@ public sealed class MqttV5PacketDecoder
{
if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier)
{
// MQTT 5.0 section 3.8.2.1.2 forbids zero and repeated identifiers.
if (packet.SubscriptionIdentifier != 0)
{
throw new MqttProtocolViolationException("Subscription Identifier must not occur more than once in a SUBSCRIBE packet.");
}
packet.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier();
if (packet.SubscriptionIdentifier == 0)
{
throw new MqttProtocolViolationException("Subscription Identifier must not be zero.");
}
}
else
{
@@ -138,12 +138,12 @@ public sealed class MqttV5PacketEncoder(MqttBufferWriter bufferWriter)
}
}
if (packet.Password.Length > 0 && packet.Username == null)
if (packet.Password != null && packet.Username == null)
{
throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22].");
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
connectFlags |= 0x40;
}
@@ -193,7 +193,7 @@ public sealed class MqttV5PacketEncoder(MqttBufferWriter bufferWriter)
_bufferWriter.WriteString(packet.Username);
}
if (packet.Password.Length > 0)
if (packet.Password != null)
{
_bufferWriter.WriteBinary(packet.Password);
}
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Exceptions;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -59,14 +60,14 @@ public struct MqttV5PropertiesReader
if (CurrentPropertyId == MqttPropertyId.UserProperty)
{
var name = _body.ReadString();
var value = _body.ReadString();
var valueBuffer = ReadUserPropertyValueBuffer();
if (CollectedUserProperties == null)
{
CollectedUserProperties = new List<MqttUserProperty>();
}
CollectedUserProperties.Add(new MqttUserProperty(name, value));
CollectedUserProperties.Add(new MqttUserProperty(name, valueBuffer));
continue;
}
@@ -190,6 +191,11 @@ public struct MqttV5PropertiesReader
return _body.ReadTwoByteInteger();
}
public ReadOnlyMemory<byte> ReadUserPropertyValueBuffer()
{
return _body.ReadBinaryData();
}
public bool ReadWildcardSubscriptionAvailable()
{
return _body.ReadByte() == 1;
@@ -29,7 +29,7 @@ public sealed class MqttV5PropertiesWriter
Write(MqttPropertyId.AssignedClientIdentifier, value);
}
public void WriteAuthenticationData(ReadOnlyMemory<byte> value)
public void WriteAuthenticationData(byte[] value)
{
Write(MqttPropertyId.AuthenticationData, value);
}
@@ -44,7 +44,7 @@ public sealed class MqttV5PropertiesWriter
Write(MqttPropertyId.ContentType, value);
}
public void WriteCorrelationData(ReadOnlyMemory<byte> value)
public void WriteCorrelationData(byte[] value)
{
Write(MqttPropertyId.CorrelationData, value);
}
@@ -260,7 +260,7 @@ public sealed class MqttV5PropertiesWriter
{
_bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty);
_bufferWriter.WriteString(property.Name);
_bufferWriter.WriteString(property.Value);
_bufferWriter.WriteString(property.ValueBuffer);
}
}
@@ -315,9 +315,9 @@ public sealed class MqttV5PropertiesWriter
_bufferWriter.WriteString(value);
}
void Write(MqttPropertyId id, ReadOnlyMemory<byte> value)
void Write(MqttPropertyId id, byte[] value)
{
if (value.Length == 0)
if (value == null)
{
return;
}
+15
View File
@@ -2,6 +2,21 @@ using MQTTnet.Diagnostics.PacketInspection;
namespace MQTTnet;
/// <summary>
/// Represents an MQTT client.
/// <para>
/// Instances of <see cref="IMqttClient" /> are safe for concurrent use: <see cref="PublishAsync" />,
/// <see cref="SubscribeAsync" />, <see cref="UnsubscribeAsync" /> and <see cref="PingAsync" /> may be
/// invoked from multiple threads at the same time. Outgoing packets are serialized internally so
/// that they reach the network in well-formed order, and incoming acknowledgements are routed back
/// to the corresponding caller via the packet identifier.
/// </para>
/// <para>
/// <see cref="ConnectAsync" /> and <see cref="DisconnectAsync" /> must not be called concurrently
/// with each other or with publish/subscribe operations. Coordinate connection lifecycle changes on
/// a single thread.
/// </para>
/// </summary>
public interface IMqttClient : IDisposable
{
event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync;
+110 -57
View File
@@ -55,6 +55,12 @@ public sealed class MqttTcpChannel : IMqttChannel
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (_tcpOptions.StreamProvider != null)
{
await ConnectViaStreamProviderAsync(cancellationToken).ConfigureAwait(false);
return;
}
CrossPlatformSocket socket = null;
try
{
@@ -103,55 +109,7 @@ public sealed class MqttTcpChannel : IMqttChannel
if (_tcpOptions.TlsOptions?.UseTls == true)
{
var targetHost = _tcpOptions.TlsOptions.TargetHost;
if (string.IsNullOrEmpty(targetHost))
{
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);
}
try
{
var sslOptions = CreateSslAuthenticationOptions();
sslOptions.TargetHost = targetHost;
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);
}
await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false);
}
catch
{
await sslStream.DisposeAsync().ConfigureAwait(false);
throw;
}
_stream = sslStream;
_stream = await WrapWithTlsAsync(networkStream, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -168,6 +126,106 @@ public sealed class MqttTcpChannel : IMqttChannel
}
}
async Task ConnectViaStreamProviderAsync(CancellationToken cancellationToken)
{
Stream providerStream = null;
try
{
providerStream = await _tcpOptions.StreamProvider.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false);
if (providerStream == null)
{
throw new MqttCommunicationException("The configured stream provider returned a null stream.");
}
cancellationToken.ThrowIfCancellationRequested();
if (_tcpOptions.TlsOptions?.UseTls == true)
{
_stream = await WrapWithTlsAsync(providerStream, cancellationToken).ConfigureAwait(false);
}
else
{
_stream = providerStream;
}
// Endpoints are diagnostic only. Prefer the provider's reported values if available;
// fall back to the broker endpoint as the "remote" so logs/diagnostics still show
// something meaningful when the provider does not expose its socket endpoints.
if (providerStream is IMqttClientStreamEndPoints connectedStream)
{
LocalEndPoint = connectedStream.LocalEndPoint;
RemoteEndPoint = connectedStream.RemoteEndPoint ?? _tcpOptions.RemoteEndpoint;
}
else
{
LocalEndPoint = null;
RemoteEndPoint = _tcpOptions.RemoteEndpoint;
}
}
catch
{
if (_stream == null)
{
providerStream?.Dispose();
}
throw;
}
}
async Task<SslStream> WrapWithTlsAsync(Stream baseStream, CancellationToken cancellationToken)
{
var targetHost = _tcpOptions.TlsOptions.TargetHost;
if (string.IsNullOrEmpty(targetHost))
{
if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns)
{
targetHost = dns.Host;
}
}
SslStream sslStream;
if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null)
{
sslStream = new SslStream(baseStream, 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(baseStream, false, InternalUserCertificateValidationCallback);
}
try
{
var sslOptions = CreateSslAuthenticationOptions();
sslOptions.TargetHost = targetHost;
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);
}
await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false);
}
catch
{
await sslStream.DisposeAsync().ConfigureAwait(false);
throw;
}
return sslStream;
}
public Task DisconnectAsync(CancellationToken cancellationToken)
{
Dispose();
@@ -304,23 +362,18 @@ public sealed class MqttTcpChannel : IMqttChannel
bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null)
{
var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
return certificateValidationHandler(eventArgs);
}
if (_tcpOptions?.TlsOptions?.IgnoreCertificateChainErrors ?? false)
{
sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors;
}
return sslPolicyErrors == SslPolicyErrors.None;
return MqttClientDefaultCertificateValidationHandler.Handle(eventArgs);
}
X509CertificateCollection LoadCertificates()
{
return _tcpOptions.TlsOptions.ClientCertificatesProvider?.GetCertificates();
}
}
}
@@ -33,6 +33,10 @@ public sealed class MqttPacketBusItem
public void Fail(Exception exception)
{
_promise.TrySetException(exception);
// Queued packets do not always have a waiter (for example, normal server dispatch).
// Observe the failure here while preserving it for callers of WaitAsync.
_ = _promise.Task.Exception;
}
public Task<MqttPacket> WaitAsync()
+1 -1
View File
@@ -25,6 +25,6 @@
<DebugType>Full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PropertyGroup Condition="'$(Configuration)'=='Release'">
</PropertyGroup>
</Project>
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Text;
@@ -263,10 +264,33 @@ public sealed class MqttApplicationMessageBuilder
/// Adds the user property to the message.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttApplicationMessageBuilder WithUserProperty(string name, string value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the message using a pre-encoded UTF-8 value buffer.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttApplicationMessageBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the message using a pre-encoded UTF-8 value buffer.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttApplicationMessageBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
+22 -2
View File
@@ -953,6 +953,21 @@ public sealed class MqttClient : Disposable, IMqttClient
{
case MqttPublishPacket publishPacket:
EnqueueReceivedPublishPacket(publishPacket);
break;
case MqttPubAckPacket _:
case MqttPubCompPacket _:
case MqttSubAckPacket _:
case MqttUnsubAckPacket _:
// Acknowledgement packets are dispatched to the awaiter that issued the
// matching request. If no awaiter is registered (for example because the
// request was cancelled or timed out client-side before the broker replied)
// the late acknowledgement is harmless and must NOT terminate the connection.
// See issues #2078 and #2079.
if (!_packetDispatcher.TryDispatch(packet))
{
_logger.Warning("Received {0} for an unknown packet identifier. Probably the matching request was cancelled or timed out.", packet.GetType().Name);
}
break;
case MqttPubRecPacket pubRecPacket:
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
@@ -1049,7 +1064,12 @@ public sealed class MqttClient : Disposable, IMqttClient
switch (exception)
{
case OperationCanceledException:
return;
if (cancellationToken.IsCancellationRequested)
{
return;
}
break;
case MqttCommunicationException:
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
break;
@@ -1065,4 +1085,4 @@ public sealed class MqttClient : Disposable, IMqttClient
_logger.Verbose("Stopped sending keep alive packets");
}
}
}
}
@@ -2,9 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using MQTTnet.Channel;
using MQTTnet.Formatter;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -251,6 +253,31 @@ public sealed class MqttClientOptionsBuilder
return this;
}
/// <summary>
/// Configures an optional <see cref="IMqttClientStreamProvider"/> on the TCP options.
/// The provider is responsible for opening the transport stream to the broker (e.g. through
/// a SOCKS5 proxy). When the connection should be TLS-secured, configure TLS via
/// <c>WithTlsOptions(...)</c>; TLS will be layered on top of the stream returned by the
/// provider, using the broker host as SNI.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when no TCP transport has been configured yet (call <c>WithTcpServer</c> or
/// <c>WithEndPoint</c> before this method).
/// </exception>
public MqttClientOptionsBuilder WithStreamProvider(IMqttClientStreamProvider provider)
{
ArgumentNullException.ThrowIfNull(provider);
if (_tcpOptions == null)
{
throw new InvalidOperationException(
"A TCP transport must be configured (via WithTcpServer or WithEndPoint) before assigning a stream provider.");
}
_tcpOptions.StreamProvider = provider;
return this;
}
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
{
if (value == MqttProtocolVersion.Unknown)
@@ -357,6 +384,7 @@ public sealed class MqttClientOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttClientOptionsBuilder WithUserProperty(string name, string value)
{
if (_options.UserProperties == null)
@@ -368,6 +396,28 @@ public sealed class MqttClientOptionsBuilder
return this;
}
public MqttClientOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWebSocketServer(Action<MqttClientWebSocketOptionsBuilder> configure)
{
ArgumentNullException.ThrowIfNull(configure);
@@ -462,10 +512,25 @@ public sealed class MqttClientOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithWillUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttClientOptionsBuilder WithWillUserProperty(string name, string value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, ReadOnlyMemory<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, ArraySegment<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -4,6 +4,7 @@
using System.Net;
using System.Net.Sockets;
using MQTTnet.Channel;
namespace MQTTnet;
@@ -44,6 +45,15 @@ public sealed class MqttClientTcpOptions : IMqttClientChannelOptions
public EndPoint RemoteEndpoint { get; set; }
/// <summary>
/// Gets or sets an optional <see cref="IMqttClientStreamProvider"/> used to obtain the
/// transport stream to the broker. When set, the channel will delegate the TCP-level
/// connect (including any proxy negotiation) to the provider and then optionally wrap the
/// returned stream with TLS, using the broker host as SNI. When <c>null</c>, the channel
/// opens a plain TCP socket directly to <see cref="RemoteEndpoint"/>.
/// </summary>
public IMqttClientStreamProvider StreamProvider { get; set; }
public MqttClientTlsOptions TlsOptions { get; set; } = new();
public override string ToString()
@@ -26,7 +26,7 @@ public class MqttEnhancedAuthenticationEventArgs : EventArgs
/// Gets the authentication data.
/// Hint: MQTT 5 feature only.
/// </summary>
public byte[] AuthenticationData => _initialAuthPacket.AuthenticationData.ToArray();
public byte[] AuthenticationData => _initialAuthPacket.AuthenticationData;
/// <summary>
/// Gets the authentication method.
@@ -67,7 +67,7 @@ public class MqttEnhancedAuthenticationEventArgs : EventArgs
{
return new ReceiveMqttEnhancedAuthenticationDataResult
{
AuthenticationData = authPacket.AuthenticationData.ToArray(),
AuthenticationData = authPacket.AuthenticationData,
AuthenticationMethod = authPacket.AuthenticationMethod,
ReasonString = authPacket.ReasonString,
ReasonCode = authPacket.ReasonCode,
+1 -1
View File
@@ -9,7 +9,7 @@ namespace MQTTnet.Packets;
/// <summary>Added in MQTTv5.0.0.</summary>
public sealed class MqttAuthPacket : MqttPacket
{
public ReadOnlyMemory<byte> AuthenticationData { get; set; }
public byte[] AuthenticationData { get; set; }
public string AuthenticationMethod { get; set; }
+1 -1
View File
@@ -13,7 +13,7 @@ public sealed class MqttConnAckPacket : MqttPacket
/// </summary>
public string AssignedClientIdentifier { get; set; }
public ReadOnlyMemory<byte> AuthenticationData { get; set; }
public byte[] AuthenticationData { get; set; }
public string AuthenticationMethod { get; set; }
+5 -6
View File
@@ -2,14 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using MQTTnet.Protocol;
namespace MQTTnet.Packets;
public sealed class MqttConnectPacket : MqttPacket
{
public ReadOnlyMemory<byte> AuthenticationData { get; set; }
public byte[] AuthenticationData { get; set; }
public string AuthenticationMethod { get; set; }
@@ -20,13 +19,13 @@ public sealed class MqttConnectPacket : MqttPacket
public string ClientId { get; set; }
public ReadOnlyMemory<byte> WillCorrelationData { get; set; }
public byte[] WillCorrelationData { get; set; }
public ushort KeepAlivePeriod { get; set; }
public uint MaximumPacketSize { get; set; }
public ReadOnlyMemory<byte> Password { get; set; }
public byte[] Password { get; set; }
public ushort ReceiveMaximum { get; set; }
@@ -50,7 +49,7 @@ public sealed class MqttConnectPacket : MqttPacket
public bool WillFlag { get; set; }
public ReadOnlySequence<byte> WillMessage { get; set; }
public byte[] WillMessage { get; set; }
public uint WillMessageExpiryInterval { get; set; }
@@ -70,7 +69,7 @@ public sealed class MqttConnectPacket : MqttPacket
{
var passwordText = string.Empty;
if (Password.Length > 0)
if (Password != null)
{
passwordText = "****";
}
+2 -2
View File
@@ -11,7 +11,7 @@ public sealed class MqttPublishPacket : MqttPacketWithIdentifier
{
public string ContentType { get; set; }
public ReadOnlyMemory<byte> CorrelationData { get; set; }
public byte[] CorrelationData { get; set; }
public bool Dup { get; set; }
@@ -19,7 +19,7 @@ public sealed class MqttPublishPacket : MqttPacketWithIdentifier
public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified;
//public ArraySegment<byte> PayloadSegment { set => Payload = new ReadOnlySequence<byte>(value); }
public ArraySegment<byte> PayloadSegment { set => Payload = new ReadOnlySequence<byte>(value); }
public ReadOnlySequence<byte> Payload { get; set; }
+53 -5
View File
@@ -2,19 +2,37 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
namespace MQTTnet.Packets;
public sealed class MqttUserProperty
{
readonly ReadOnlyMemory<byte> _valueBuffer;
[Obsolete("Please use more performance constructor with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttUserProperty(string name, string value)
: this(name, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(value ?? throw new ArgumentNullException(nameof(value)))))
{
}
public MqttUserProperty(string name, ArraySegment<byte> value)
: this(name, CreateMemory(value))
{
}
public MqttUserProperty(string name, ReadOnlyMemory<byte> value)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Value = value ?? throw new ArgumentNullException(nameof(value));
_valueBuffer = value;
}
public string Name { get; }
public string Value { get; }
public ReadOnlyMemory<byte> ValueBuffer => _valueBuffer;
[Obsolete("Please use more performance property ValueBuffer or the MqttUserPropertyExtensionMethod `ReadValueAsString`")]
public string Value => this.ReadValueAsString();
public override bool Equals(object obj)
{
@@ -33,16 +51,46 @@ public sealed class MqttUserProperty
return true;
}
return string.Equals(Name, other.Name, StringComparison.Ordinal) && string.Equals(Value, other.Value, StringComparison.Ordinal);
if (!string.Equals(Name, other.Name, StringComparison.Ordinal))
{
return false;
}
return _valueBuffer.Span.SequenceEqual(other._valueBuffer.Span);
}
public override int GetHashCode()
{
return Name.GetHashCode() ^ Value.GetHashCode();
var hashCode = new HashCode();
if (!string.IsNullOrEmpty(Name))
{
hashCode.Add(Name, StringComparer.Ordinal);
}
else
{
hashCode.Add(0);
}
hashCode.AddBytes(_valueBuffer.Span);
return hashCode.ToHashCode();
}
public override string ToString()
{
return $"{Name} = {Value}";
return $"{Name} = {this.ReadValueAsString()}";
}
static ReadOnlyMemory<byte> CreateMemory(ArraySegment<byte> value)
{
if (value.Array == null)
{
throw new ArgumentNullException(nameof(value));
}
return new ReadOnlyMemory<byte>(value.Array, value.Offset, value.Count);
}
}
@@ -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;
using System.Text;
namespace MQTTnet.Packets;
public static class MqttUserPropertyExtensions
{
/// <summary>
/// Reads the value of the user property as a UTF-8 string.
/// </summary>
public static string ReadValueAsString(this MqttUserProperty userProperty)
{
ArgumentNullException.ThrowIfNull(userProperty);
var buffer = userProperty.ValueBuffer;
if (buffer.IsEmpty)
{
return string.Empty;
}
return Encoding.UTF8.GetString(buffer.Span);
}
}
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Exceptions;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -77,6 +78,7 @@ public sealed class MqttClientSubscribeOptionsBuilder
/// Adds the user property to the subscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value)
{
_subscribeOptions.UserProperties ??= [];
@@ -84,4 +86,28 @@ public sealed class MqttClientSubscribeOptionsBuilder
return this;
}
/// <summary>
/// Adds the user property to the subscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
_subscribeOptions.UserProperties ??= [];
_subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the subscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_subscribeOptions.UserProperties ??= [];
_subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -2,6 +2,7 @@
// The .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;
@@ -36,11 +37,30 @@ public sealed class MqttClientUnsubscribeOptionsBuilder
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, string value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
+4 -9
View File
@@ -1,9 +1,4 @@
* Core: Used new language features across the entire library
* Core: Performance improvements
* Core: Added validations for variable byte integers which do not match the .NET uint perfectly
* Server: Improved performance of retained messages when no event handler is attached (#2093, thanks to @zhaowgit)
* Server: The event `InterceptingClientEnqueue` is now also called for retained messages (BREAKING CHANGE!)
* Server: The local end point is now also exposed in the channel adapter (#2179)
* Marked all projects as AOT compatible
* Restored the strong name of the nugets
* Embedded license file in all nugets (#2197, thanks to @JensNordenbro)
* Client: Late acknowledgement packets (PUBACK, PUBCOMP, SUBACK, UNSUBACK) for canceled or timed-out requests no longer trigger a protocol violation and tear down the connection (#2078, #2079)
* Client: Add support for SOCKS5 proxies (#2251, thanks to @koepalex)
* Client: Fixed keep alive ping send timeout disconnect handling (#2253, thanks to @suhashollakc)
* Server: Fixed exposing Topic Alias to clients (#2250, thanks to @suhashollakc)
+5
View File
@@ -0,0 +1,5 @@
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}