Compare commits

..
Author SHA1 Message Date
Christian 2fd8e95b54 Avoid using obsolete code 2024-02-29 18:35:13 +01:00
59 changed files with 429 additions and 2595 deletions
+2 -1
View File
@@ -40,6 +40,7 @@ dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
# ReSharper properties
resharper_autodetect_indent_settings = true
resharper_default_private_modifier = implicit
resharper_place_accessorholder_attribute_on_same_line = false
resharper_use_indent_from_vs = false
@@ -60,7 +61,7 @@ resharper_web_config_wrong_module_highlighting = warning
[*]
charset = utf-8
end_of_line = crlf
trim_trailing_whitespace = true
trim_trailing_whitespace = false
insert_final_newline = false
indent_style = space
indent_size = 4
+5 -3
View File
@@ -1,3 +1,5 @@
* Client: Fixed wrong timeout for keep alive check (thanks to @Erw1nT, #2129)
* Client: Fixed extended authentication (thanks to @chkr1011, #2132).
* Server: Fixed extended authentication (thanks to @chkr1011, #2132).
* [Client] Exposed the _EndPoint_ type to support other endpoint types (like Unix Domain Sockets) in client options (#1919).
* [ManagedClient] Added support for intercepting (and cancelling) publish messages (#1915, thanks to @hannasm).
* [Server] Added new events for delivered and dropped messages (#1866, thanks to @kallayj).
* [Server] The server will no longer treat a client which is receiving a large payload as alive. The packet must be received completely within the keep alive boundaries (BREAKING CHANGE!, #1883).
* [Server] Fixed "service not registered" exception in ASP.NET integration (#1889).
+13 -31
View File
@@ -3,7 +3,7 @@ name: CI
on: [push, pull_request]
env:
VERSION: "4.3.8.${{github.run_number}}"
VERSION: "4.3.3.${{github.run_number}}"
jobs:
build:
@@ -12,12 +12,12 @@ jobs:
steps:
- name: Setup Windows SDK
uses: GuillaumeFalourd/setup-windows10-sdk-action@v2.4
uses: GuillaumeFalourd/setup-windows10-sdk-action@v1
with:
sdk-version: 18362
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v1.9.0
with:
dotnet-version: |
3.1.x
@@ -28,7 +28,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.1
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Setup Signing Certificate
run: |
@@ -42,6 +42,14 @@ jobs:
- name: Build solution
run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk
- name: Collect nuget Packages
uses: actions/upload-artifact@v2
with:
name: nuget Packages
path: |
**\*.nupkg
**\*.snupkg
- name: Setup VSTest
uses: darenm/Setup-VSTest@v1
@@ -51,32 +59,6 @@ jobs:
- name: ASP.NET Tests
run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll
- name: Install sign CLI tool
run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3
- name: Sign nugets
shell: pwsh
run: >
./sign code azure-key-vault `
"**/*.nupkg" `
--base-directory "${{ github.workspace }}/Source" `
--publisher-name "MQTTnet" `
--description "MQTTnet" `
--description-url "https://github.com/dotnet/MQTTnet" `
--azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" `
--azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" `
--azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" `
--azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" `
--azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}"
- name: Upload signed nuget packages
uses: actions/upload-artifact@v4
with:
name: signed-nugets
path: |
${{ github.workspace }}/Source/**/*.nupkg
${{ github.workspace }}/Source/**/*.snupkg
- name: Publish MyGet nugets
if: ${{ github.event_name == 'push' }}
run: dotnet nuget push ${{ github.workspace }}/Source/**/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
run: dotnet nuget push **/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate
+9 -15
View File
@@ -7,11 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\M
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}"
ProjectSection(SolutionItems) = preProject
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
LICENSE = LICENSE
README.md = README.md
.github\workflows\ReleaseNotes.md = .github\workflows\ReleaseNotes.md
.github\workflows\ci.yml = .github\workflows\ci.yml
CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}"
@@ -22,19 +20,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -86,14 +82,12 @@ Global
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374D5861-F7A6-42A2-8CBE-040846000EF9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894}
EndGlobalSection
-102
View File
@@ -8,13 +8,10 @@
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using MQTTnet.Client;
using MQTTnet.Extensions.WebSocket4Net;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
using MQTTnet.Samples.Helpers;
using MQTTnet.Server;
namespace MQTTnet.Samples.Client;
@@ -103,105 +100,6 @@ public static class Client_Connection_Samples
}
}
public static async Task Connect_Using_Extended_Authentication()
{
/*
* This sample uses enhanced (aka extended) authentication (Kerberos) when creating the connection.
*/
/*
* Server part...
*/
var mqttFactory = new MqttFactory();
var serverOptions = mqttFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build();
var server = mqttFactory.CreateMqttServer(serverOptions);
server.ValidatingConnectionAsync += async args =>
{
if (args.AuthenticationMethod == "GS2-KRB5")
{
var result = await args.SendExtendedAuthenticationExchangeDataAsync(new MqttExtendedAuthenticationExchangeOptions());
Console.WriteLine($"Received AUTH data from client: {Encoding.UTF8.GetString(result.AuthenticationData)}");
var authOptions = mqttFactory.CreateMqttExtendedAuthenticationExchangeOptionsBuilder().WithAuthenticationData("reply context token").Build();
result = await args.SendExtendedAuthenticationExchangeDataAsync(authOptions);
Console.WriteLine($"Received AUTH data from client: {Encoding.UTF8.GetString(result.AuthenticationData)}");
args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray();
// Authentication DONE!
args.ReasonCode = MqttConnectReasonCode.Success; // Also the default!
}
else
{
args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod;
}
};
await server.StartAsync();
/*
* Client part...
*/
var mqttClientFactory = new MqttFactory();
// Use Kerberos sample from the MQTT RFC.
var kerberosAuthenticationHandler = new SampleClientKerberosAuthenticationHandler();
var clientOptions = mqttClientFactory.CreateClientOptionsBuilder()
.WithTcpServer("localhost")
.WithProtocolVersion(MqttProtocolVersion.V500)
.WithExtendedAuthenticationExchangeHandler(kerberosAuthenticationHandler)
.WithAuthentication("GS2-KRB5", Array.Empty<byte>())
.Build();
var client = mqttClientFactory.CreateMqttClient();
var result = await client.ConnectAsync(clientOptions);
Console.WriteLine($"Client connect result: {result.ResultCode}");
}
class SampleClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler
{
private readonly Queue<byte[]> clientTokens = new Queue<byte[]>();
public SampleClientKerberosAuthenticationHandler()
{
// The Kerberos sample from the MQTT RFC sends two client tokens after the initial empty token.
clientTokens.Enqueue(Encoding.UTF8.GetBytes("initial context token"));
clientTokens.Enqueue(Array.Empty<byte>());
}
public async Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context)
{
if (context.AuthenticationMethod != "GS2-KRB5")
{
throw new InvalidOperationException("Wrong authentication method");
}
Console.WriteLine($"Received AUTH data from server: {Encoding.UTF8.GetString(context.AuthenticationData)}");
if (!clientTokens.TryDequeue(out var clientToken))
{
throw new InvalidOperationException("No more client tokens available");
}
var sendOptions = new MqttExtendedAuthenticationExchangeData
{
AuthenticationData = clientToken
};
await context.Client.SendExtendedAuthenticationExchangeDataAsync(sendOptions);
}
}
public static async Task Connect_Client_Using_MQTTv5()
{
/*
+30 -13
View File
@@ -8,7 +8,6 @@
// ReSharper disable UnusedMember.Local
using MQTTnet.Client;
using MQTTnet.Extensions.TopicTemplate;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Samples.Helpers;
@@ -17,8 +16,6 @@ namespace MQTTnet.Samples.Client;
public static class Client_Subscribe_Samples
{
static MqttTopicTemplate sampleTemplate = new MqttTopicTemplate("mqttnet/samples/topic/{id}");
public static async Task Handle_Received_Application_Message()
{
/*
@@ -45,7 +42,11 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicTemplate(sampleTemplate.WithParameter("id", "2"))
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2");
})
.Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -87,8 +88,11 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicTemplate(
sampleTemplate.WithParameter("id", "1"))
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -117,12 +121,21 @@ public static class Client_Subscribe_Samples
// Create the subscribe options including several topics with different options.
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicTemplate(
sampleTemplate.WithParameter("id", "1"))
.WithTopicTemplate(
sampleTemplate.WithParameter("id", "2"), noLocal: true)
.WithTopicTemplate(
sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2").WithNoLocal();
})
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/3").WithRetainHandling(MqttRetainHandling.SendAtSubscribe);
})
.Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -149,7 +162,11 @@ public static class Client_Subscribe_Samples
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/1");
})
.Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
-1
View File
@@ -17,7 +17,6 @@
<ProjectReference Include="..\Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
<ProjectReference Include="..\Source\MQTTnet\MQTTnet.csproj" />
</ItemGroup>
@@ -1,87 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable InconsistentNaming
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
namespace MQTTnet.Samples.ManagedClient;
public sealed class Managed_Client_Subscribe_Samples
{
public static async Task Connect_Client()
{
/*
* This sample creates a simple managed MQTT client and connects to a public broker, subscribe to a topic and verifies subscription result.
*
* The managed client extends the existing _MqttClient_. It adds the following features.
* - Reconnecting when connection is lost.
* - Storing pending messages in an internal queue so that an enqueue is possible while the client remains not connected.
*/
var mqttFactory = new MqttFactory();
var subscribed = false;
using (var managedMqttClient = mqttFactory.CreateManagedMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer("broker.hivemq.com")
.Build();
var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder()
.WithClientOptions(mqttClientOptions)
.Build();
await managedMqttClient.StartAsync(managedMqttClientOptions);
// The application message is not sent. It is stored in an internal queue and
// will be sent when the client is connected.
await managedMqttClient.EnqueueAsync("Topic", "Payload");
Console.WriteLine("The managed MQTT client is connected.");
// Wait until the queue is fully processed.
SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000);
Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}");
managedMqttClient.SubscriptionsChangedAsync += args => SubscriptionsResultAsync(args, ref subscribed);
await managedMqttClient.SubscribeAsync("Topic").ConfigureAwait(false);
SpinWait.SpinUntil(() => subscribed, 1000);
Console.WriteLine($"Subscription properly done {subscribed}");
}
}
private static Task SubscriptionsResultAsync(SubscriptionsChangedEventArgs arg, ref bool subscribed)
{
foreach (var mqttClientSubscribeResult in arg.SubscribeResult)
{
Console.WriteLine($"Subscription reason {mqttClientSubscribeResult.ReasonString}");
foreach (var item in mqttClientSubscribeResult.Items)
{
Console.WriteLine($"For topic filter {item.TopicFilter}, result code: {item.ResultCode}");
if (item.TopicFilter.Topic == "Topic" && item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS0 && !subscribed)
{
subscribed = true;
}
}
}
foreach (var mqttClientUnsubscribeResult in arg.UnsubscribeResult)
{
Console.WriteLine($"Unsubscription reason {mqttClientUnsubscribeResult.ReasonString}");
foreach (var item in mqttClientUnsubscribeResult.Items)
{
Console.WriteLine($"For topic filter {item.TopicFilter}, result code: {item.ResultCode}");
}
}
return Task.CompletedTask;
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
@@ -42,7 +42,7 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
@@ -51,11 +51,11 @@
<DefineConstants>RELEASE;NETSTANDARD2_0</DefineConstants>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0' and '$(TargetFramework)' != 'netcoreapp2.1' ">
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp2.1' ">
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1' and '$(TargetFramework)' != 'net5.0' and '$(TargetFramework)' != 'net6.0' and '$(TargetFramework)' != 'net7.0' ">
<PackageReference Include="Microsoft.AspNetCore.Http.Connections" Version="1.1.0" />
</ItemGroup>
+8 -33
View File
@@ -16,51 +16,26 @@ namespace MQTTnet.AspNetCore
public sealed class MqttHostedServer : MqttServer, IHostedService
{
readonly MqttFactory _mqttFactory;
#if NETCOREAPP3_1_OR_GREATER
readonly IHostApplicationLifetime _hostApplicationLifetime;
public MqttHostedServer(IHostApplicationLifetime hostApplicationLifetime, MqttFactory mqttFactory,
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
options,
adapters,
logger)
{
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
_hostApplicationLifetime = hostApplicationLifetime;
}
#else
public MqttHostedServer(MqttFactory mqttFactory,
MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
options,
adapters,
logger)
{
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
}
#endif
public MqttHostedServer(MqttFactory mqttFactory, MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger) : base(
options,
adapters,
logger)
{
_mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// The yield makes sure that the hosted service is considered up and running.
await Task.Yield();
#if NETCOREAPP3_1_OR_GREATER
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
#else
_ = StartAsync();
#endif
_ = StartAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
}
#if NETCOREAPP3_1_OR_GREATER
private void OnStarted()
{
_ = StartAsync();
}
#endif
}
}
@@ -40,10 +40,13 @@ namespace MQTTnet.AspNetCore
throw new ArgumentNullException(nameof(services));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
var serverOptionsBuilder = new MqttServerOptionsBuilder();
configure?.Invoke(serverOptionsBuilder);
configure.Invoke(serverOptionsBuilder);
var options = serverOptionsBuilder.Build();
return AddHostedMqttServer(services, options);
@@ -25,8 +25,6 @@ namespace MQTTnet.Extensions.ManagedClient
event Func<ManagedProcessFailedEventArgs, Task> SynchronizingSubscriptionsFailedAsync;
event Func<SubscriptionsChangedEventArgs, Task> SubscriptionsChangedAsync;
IMqttClient InternalClient { get; }
bool IsConnected { get; }
@@ -47,8 +45,8 @@ namespace MQTTnet.Extensions.ManagedClient
Task StopAsync(bool cleanDisconnect = true);
Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters);
Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters);
Task UnsubscribeAsync(IEnumerable<string> topics);
Task UnsubscribeAsync(ICollection<string> topics);
}
}
@@ -19,17 +19,16 @@ namespace MQTTnet.Extensions.ManagedClient
{
public sealed class ManagedMqttClient : Disposable, IManagedMqttClient
{
readonly MqttNetSourceLogger _logger;
readonly AsyncEvent<InterceptingPublishMessageEventArgs> _interceptingPublishMessageEvent = new AsyncEvent<InterceptingPublishMessageEventArgs>();
readonly AsyncEvent<ApplicationMessageProcessedEventArgs> _applicationMessageProcessedEvent = new AsyncEvent<ApplicationMessageProcessedEventArgs>();
readonly AsyncEvent<ApplicationMessageSkippedEventArgs> _applicationMessageSkippedEvent = new AsyncEvent<ApplicationMessageSkippedEventArgs>();
readonly AsyncEvent<ConnectingFailedEventArgs> _connectingFailedEvent = new AsyncEvent<ConnectingFailedEventArgs>();
readonly AsyncEvent<EventArgs> _connectionStateChangedEvent = new AsyncEvent<EventArgs>();
readonly AsyncEvent<ManagedProcessFailedEventArgs> _synchronizingSubscriptionsFailedEvent = new AsyncEvent<ManagedProcessFailedEventArgs>();
readonly AsyncEvent<SubscriptionsChangedEventArgs> _subscriptionsChangedEvent = new AsyncEvent<SubscriptionsChangedEventArgs>();
readonly MqttNetSourceLogger _logger;
readonly BlockingQueue<ManagedMqttApplicationMessage> _messageQueue = new BlockingQueue<ManagedMqttApplicationMessage>();
readonly AsyncLock _messageQueueLock = new AsyncLock();
/// <summary>
@@ -83,19 +82,21 @@ namespace MQTTnet.Extensions.ManagedClient
add => _applicationMessageSkippedEvent.AddHandler(value);
remove => _applicationMessageSkippedEvent.RemoveHandler(value);
}
public event Func<ApplicationMessageProcessedEventArgs, Task> ApplicationMessageProcessedAsync
{
add => _applicationMessageProcessedEvent.AddHandler(value);
remove => _applicationMessageProcessedEvent.RemoveHandler(value);
}
public event Func<InterceptingPublishMessageEventArgs, Task> InterceptPublishMessageAsync
{
add => _interceptingPublishMessageEvent.AddHandler(value);
remove => _interceptingPublishMessageEvent.RemoveHandler(value);
}
public event Func<MqttApplicationMessageReceivedEventArgs, Task> ApplicationMessageReceivedAsync
{
add => InternalClient.ApplicationMessageReceivedAsync += value;
@@ -132,12 +133,6 @@ namespace MQTTnet.Extensions.ManagedClient
remove => _synchronizingSubscriptionsFailedEvent.RemoveHandler(value);
}
public event Func<SubscriptionsChangedEventArgs, Task> SubscriptionsChangedAsync
{
add => _subscriptionsChangedEvent.AddHandler(value);
remove => _subscriptionsChangedEvent.RemoveHandler(value);
}
public IMqttClient InternalClient { get; }
public bool IsConnected => InternalClient.IsConnected;
@@ -147,7 +142,7 @@ namespace MQTTnet.Extensions.ManagedClient
public ManagedMqttClientOptions Options { get; private set; }
public int PendingApplicationMessagesCount => _messageQueue.Count;
public async Task EnqueueAsync(MqttApplicationMessage applicationMessage)
{
ThrowIfDisposed();
@@ -275,7 +270,7 @@ namespace MQTTnet.Extensions.ManagedClient
ThrowIfDisposed();
_isCleanDisconnect = cleanDisconnect;
StopPublishing();
StopMaintainingConnection();
@@ -288,7 +283,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
}
public Task SubscribeAsync(IEnumerable<MqttTopicFilter> topicFilters)
public Task SubscribeAsync(ICollection<MqttTopicFilter> topicFilters)
{
ThrowIfDisposed();
@@ -316,7 +311,7 @@ namespace MQTTnet.Extensions.ManagedClient
return CompletedTask.Instance;
}
public Task UnsubscribeAsync(IEnumerable<string> topics)
public Task UnsubscribeAsync(ICollection<string> topics)
{
ThrowIfDisposed();
@@ -367,13 +362,6 @@ namespace MQTTnet.Extensions.ManagedClient
return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime;
}
CancellationTokenSource NewTimeoutToken(CancellationToken linkedToken)
{
var newTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource(linkedToken);
newTimeoutToken.CancelAfter(Options.ClientOptions.Timeout);
return newTimeoutToken;
}
async Task HandleSubscriptionExceptionAsync(Exception exception, List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{
_logger.Warning(exception, "Synchronizing subscriptions failed.");
@@ -384,14 +372,6 @@ namespace MQTTnet.Extensions.ManagedClient
}
}
async Task HandleSubscriptionsResultAsync(SendSubscribeUnsubscribeResult subscribeUnsubscribeResult)
{
if (_subscriptionsChangedEvent.HasHandlers)
{
await _subscriptionsChangedEvent.InvokeAsync(new SubscriptionsChangedEventArgs(subscribeUnsubscribeResult.SubscribeResults, subscribeUnsubscribeResult.UnsubscribeResults)).ConfigureAwait(false);
}
}
async Task MaintainConnectionAsync(CancellationToken cancellationToken)
{
try
@@ -416,7 +396,7 @@ namespace MQTTnet.Extensions.ManagedClient
{
if (_isCleanDisconnect)
{
using (var disconnectTimeout = NewTimeoutToken(CancellationToken.None))
using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
{
await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false);
}
@@ -466,7 +446,7 @@ namespace MQTTnet.Extensions.ManagedClient
cancellationToken.ThrowIfCancellationRequested();
await TryPublishQueuedMessageAsync(message, cancellationToken).ConfigureAwait(false);
await TryPublishQueuedMessageAsync(message).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
@@ -482,7 +462,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
}
async Task PublishReconnectSubscriptionsAsync(CancellationToken cancellationToken)
async Task PublishReconnectSubscriptionsAsync()
{
_logger.Info("Publishing subscriptions at reconnect");
@@ -493,7 +473,6 @@ namespace MQTTnet.Extensions.ManagedClient
if (_reconnectSubscriptions.Any())
{
topicFilters = new List<MqttTopicFilter>();
SendSubscribeUnsubscribeResult subscribeUnsubscribeResult;
foreach (var sub in _reconnectSubscriptions)
{
@@ -501,14 +480,12 @@ namespace MQTTnet.Extensions.ManagedClient
if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
topicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
}
}
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(topicFilters, null, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false);
}
}
catch (Exception exception)
@@ -524,7 +501,6 @@ namespace MQTTnet.Extensions.ManagedClient
while (await _subscriptionsQueuedSignal.WaitAsync(GetRemainingTime(endTime), cancellationToken).ConfigureAwait(false))
{
List<MqttTopicFilter> subscriptions;
SendSubscribeUnsubscribeResult subscribeUnsubscribeResult;
HashSet<string> unsubscriptions;
lock (_subscriptions)
@@ -560,14 +536,12 @@ namespace MQTTnet.Extensions.ManagedClient
if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
addedTopicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
}
}
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(addedTopicFilters, null, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false);
var removedTopicFilters = new List<string>();
foreach (var unSub in unsubscriptions)
@@ -576,14 +550,12 @@ namespace MQTTnet.Extensions.ManagedClient
if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets)
{
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
removedTopicFilters.Clear();
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
}
}
subscribeUnsubscribeResult = await SendSubscribeUnsubscribe(null, removedTopicFilters, cancellationToken).ConfigureAwait(false);
await HandleSubscriptionsResultAsync(subscribeUnsubscribeResult).ConfigureAwait(false);
await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false);
}
}
@@ -597,7 +569,7 @@ namespace MQTTnet.Extensions.ManagedClient
MqttClientConnectResult connectResult = null;
try
{
using (var connectTimeout = NewTimeoutToken(cancellationToken))
using (var connectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout))
{
connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false);
}
@@ -616,10 +588,8 @@ namespace MQTTnet.Extensions.ManagedClient
}
}
async Task<SendSubscribeUnsubscribeResult> SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions, CancellationToken cancellationToken)
async Task SendSubscribeUnsubscribe(List<MqttTopicFilter> addedSubscriptions, List<string> removedSubscriptions)
{
var subscribeResults = new List<MqttClientSubscribeResult>();
var unsubscribeResults = new List<MqttClientUnsubscribeResult>();
try
{
if (removedSubscriptions != null && removedSubscriptions.Any())
@@ -631,11 +601,7 @@ namespace MQTTnet.Extensions.ManagedClient
unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription);
}
using (var unsubscribeTimeout = NewTimeoutToken(cancellationToken))
{
var unsubscribeResult = await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build(), unsubscribeTimeout.Token).ConfigureAwait(false);
unsubscribeResults.Add(unsubscribeResult);
}
await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false);
//clear because these worked, maybe the subscribe below will fail, only report those
removedSubscriptions.Clear();
@@ -650,19 +616,13 @@ namespace MQTTnet.Extensions.ManagedClient
subscribeOptionsBuilder.WithTopicFilter(addedSubscription);
}
using (var subscribeTimeout = NewTimeoutToken(cancellationToken))
{
var subscribeResult = await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build(), subscribeTimeout.Token).ConfigureAwait(false);
subscribeResults.Add(subscribeResult);
}
await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build()).ConfigureAwait(false);
}
}
catch (Exception exception)
{
await HandleSubscriptionExceptionAsync(exception, addedSubscriptions, removedSubscriptions).ConfigureAwait(false);
}
return new SendSubscribeUnsubscribeResult(subscribeResults, unsubscribeResults);
}
void StartPublishing()
@@ -708,7 +668,7 @@ namespace MQTTnet.Extensions.ManagedClient
{
var oldConnectionState = InternalClient.IsConnected;
var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false);
if (connectionState == ReconnectionResult.NotConnected)
{
StopPublishing();
@@ -716,7 +676,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
else if (connectionState == ReconnectionResult.Reconnected)
{
await PublishReconnectSubscriptionsAsync(cancellationToken).ConfigureAwait(false);
await PublishReconnectSubscriptionsAsync().ConfigureAwait(false);
StartPublishing();
}
else if (connectionState == ReconnectionResult.Recovered)
@@ -746,7 +706,7 @@ namespace MQTTnet.Extensions.ManagedClient
}
}
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message, CancellationToken cancellationToken)
async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message)
{
Exception transmitException = null;
bool acceptPublish = true;
@@ -761,10 +721,7 @@ namespace MQTTnet.Extensions.ManagedClient
if (acceptPublish)
{
using (var publishTimeout = NewTimeoutToken(cancellationToken))
{
await InternalClient.PublishAsync(message.ApplicationMessage, publishTimeout.Token).ConfigureAwait(false);
}
await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false);
}
using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync
@@ -1,23 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using MQTTnet.Client;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class SendSubscribeUnsubscribeResult
{
public SendSubscribeUnsubscribeResult(List<MqttClientSubscribeResult> subscribeResults, List<MqttClientUnsubscribeResult> unsubscribeResults)
{
SubscribeResults = subscribeResults ?? throw new ArgumentNullException(nameof(subscribeResults));
UnsubscribeResults = unsubscribeResults ?? throw new ArgumentNullException(nameof(unsubscribeResults));
}
public List<MqttClientSubscribeResult> SubscribeResults { get; private set; }
public List<MqttClientUnsubscribeResult> UnsubscribeResults { get; private set; }
}
}
@@ -1,23 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Client;
using System;
using System.Collections.Generic;
namespace MQTTnet.Extensions.ManagedClient
{
public sealed class SubscriptionsChangedEventArgs : EventArgs
{
public SubscriptionsChangedEventArgs(List<MqttClientSubscribeResult> subscribeResult, List<MqttClientUnsubscribeResult> unsubscribeResult)
{
SubscribeResult = subscribeResult ?? throw new ArgumentNullException(nameof(subscribeResult));
UnsubscribeResult = unsubscribeResult ?? throw new ArgumentNullException(nameof(unsubscribeResult));
}
public List<MqttClientSubscribeResult> SubscribeResult { get; }
public List<MqttClientUnsubscribeResult> UnsubscribeResult { get; }
}
}
@@ -1,7 +1,3 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
@@ -1,73 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461;net48</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
<AssemblyName>MQTTnet.Extensions.TopicTemplate</AssemblyName>
<RootNamespace>MQTTnet.Extensions.TopicTemplate</RootNamespace>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Company>The contributors of MQTTnet</Company>
<Product>MQTTnet</Product>
<Description>
Provides mqtt topic templating logic to support dispatch,
routing and similar functionality based on the well known moustache syntax (AsyncAPI compatible).
</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>The contributors of MQTTnet</Authors>
<PackageId>MQTTnet.Extensions.TopicTemplate</PackageId>
<SignAssembly>false</SignAssembly>
<DelaySign>false</DelaySign>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>MQTT Message Queue Telemetry MQTTClient Messaging Routing AsyncAPI Template</PackageTags>
<NeutralLanguage>en-US</NeutralLanguage>
<PackageIcon>nuget.png</PackageIcon>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/).</PackageReleaseNotes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591;NETSDK1138</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'uap10.0'">
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<DefineConstants>$(DefineConstants);WINDOWS_UWP</DefineConstants>
<DefaultLanguage>en</DefaultLanguage>
<LanguageTargets>$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets</LanguageTargets>
</PropertyGroup>
<ItemGroup>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\Images\nuget.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj" />
</ItemGroup>
</Project>
@@ -1,357 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate
{
/// <summary>
/// A topic template is an MQTT topic filter string that may contain
/// segments in curly braces called parameters. This well-known
/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
/// The topic template is designed to support dynamic subscription/publication,
/// message-topic matching and routing. It is intended to be more safe and
/// convenient than String.Format() for aforementioned purposes.
/// </summary>
/// <example>
/// topic/subtopic/{parameter}/{otherParameter}
/// </example>
public sealed class MqttTopicTemplate : IEquatable<MqttTopicTemplate>
{
static readonly Regex MoustacheRegex = new Regex("{([^/]+?)}", RegexOptions.Compiled);
readonly string[] _parameterSegments;
string _topicFilter;
/// <summary>
/// Create a topic template from an mqtt topic filter with moustache placeholders.
/// </summary>
/// <param
/// name="topicTemplate">
/// </param>
/// <exception
/// cref="ArgumentNullException">
/// </exception>
public MqttTopicTemplate(string topicTemplate)
{
if (topicTemplate == null)
{
throw new ArgumentNullException(nameof(topicTemplate));
}
MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
Template = topicTemplate;
_parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
.Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
.Select(s => s.Length > 0 ? s : null)
.ToArray();
}
/// <summary>
/// Yield the template parameter names.
/// </summary>
public IEnumerable<string> Parameters => _parameterSegments.Where(s => s != null);
/// <summary>
/// The topic template string representation, e.g. A/B/{foo}/D.
/// </summary>
public string Template { get; }
/// <summary>
/// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
/// ends with a multi-level wildcard (hash), this will be reflected here.
/// </summary>
public string TopicFilter
{
get
{
LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
return _topicFilter;
}
}
/// <summary>
/// Return the topic filter of this template, ending with a multi-level wildcard (hash).
/// </summary>
public string TopicTreeRootFilter
{
get
{
var filter = TopicFilter;
// append slash if neccessary
if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.LevelSeparator;
}
// append hash if neccessary
if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
filter += MqttTopicFilterComparer.MultiLevelWildcard;
}
return filter;
}
}
public bool Equals(MqttTopicTemplate other)
{
return other != null && Template == other.Template;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((MqttTopicTemplate)obj);
}
/// <summary>
/// Determine the shortest common prefix of the given templates. Partial segments
/// are not returned.
/// </summary>
/// <param
/// name="templates">
/// topic templates
/// </param>
/// <returns></returns>
public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable<MqttTopicTemplate> templates)
{
string root = null;
string CommonPrefix(string a, string b)
{
var maxIndex = Math.Min(a.Length, b.Length) - 1;
for (var i = 0; i <= maxIndex; i++)
{
if (a[i] != b[i])
{
return a.Substring(0, i);
}
}
return a.Substring(0, maxIndex+1);
}
foreach (string topic in from template in templates select template.Template)
{
root = root == null ? topic : CommonPrefix(root, topic);
}
if (string.IsNullOrEmpty(root))
return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
if (root.Contains(MqttTopicFilterComparer.LevelSeparator) &&
!root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
!root.EndsWith("}"))
{
root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator)+1);
}
if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
root += MqttTopicFilterComparer.SingleLevelWildcard;
return new MqttTopicTemplate(root);
}
public override int GetHashCode()
{
return Template.GetHashCode();
}
/// <summary>
/// Test if this topic template matches a given topic.
/// </summary>
/// <param
/// name="topic">
/// a fully specified topic
/// </param>
/// <param
/// name="subtree">
/// true to match including the subtree (multi-level wildcard)
/// </param>
/// <returns>true iff the topic matches the template's filter</returns>
/// <exception
/// cref="InvalidOperationException">
/// </exception>
/// <exception
/// cref="ArgumentException">
/// if the topic is invalid
/// </exception>
public bool MatchesTopic(string topic, bool subtree = false)
{
var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
{
throw new InvalidOperationException("Invalid filter");
}
if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
{
throw new ArgumentException("Invalid topic", nameof(topic));
}
return comparison == MqttTopicFilterCompareResult.IsMatch;
}
/// <summary>
/// Extract the parameter values from a topic corresponding to the template
/// parameters. The topic has to match this template.
/// </summary>
/// <param
/// name="topic">
/// the topic
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
{
if (!MatchesTopic(topic))
{
throw new ArgumentException("the topic has to match this template", nameof(topic));
}
return parseParameterValuesInternal(topic);
}
/// <summary>
/// Extract the parameter values from the message topic corresponding to the template
/// parameters. The message topic has to match this topic template.
/// </summary>
/// <param
/// name="message">
/// the message
/// </param>
/// <returns>an enumeration of (parameter, index, value)</returns>
public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
{
return ParseParameterValues(message.Topic);
}
/// <summary>
/// Try to set a parameter to a given value. If the parameter is not present,
/// this is returned. The value must not contain slashes.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <returns></returns>
public MqttTopicTemplate TrySetParameter(string parameter, string value)
{
if (parameter != null && _parameterSegments.Contains(parameter))
{
return WithParameter(parameter, value);
}
return this;
}
/// <summary>
/// Replace the given parameter with a single-level wildcard (plus sign).
/// </summary>
/// <param
/// name="parameter">
/// parameter name
/// </param>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithoutParameter(string parameter)
{
if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
{
throw new ArgumentException("topic template parameter must exist.");
}
return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
}
/// <summary>
/// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
/// the method trows. The value must not contain slashes or wildcards.
/// </summary>
/// <param
/// name="parameter">
/// a template parameter
/// </param>
/// <param
/// name="value">
/// a string
/// </param>
/// <exception
/// cref="ArgumentException">
/// when the parameter is not present
/// </exception>
/// <returns>the topic template (without the parameter)</returns>
public MqttTopicTemplate WithParameter(string parameter, string value)
{
if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) ||
value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) ||
value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
{
throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
}
return ReplaceInternal(parameter, value);
}
private MqttTopicTemplate ReplaceInternal(string parameter, string value)
{
var moustache = "{" + parameter + "}";
return new MqttTopicTemplate(Template.Replace(moustache, value));
}
/// <summary>
/// Reuse parameters as they are extracted using another topic template on this template
/// when the parameter name matches. Useful
/// for compatibility routing.
/// </summary>
/// <param
/// name="parameters">
/// </param>
/// <returns></returns>
public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
{
return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
}
IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
{
// because we have a match, we know the segment array is at least the template's length
var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
for (var i = 0; i < _parameterSegments.Length; i++)
{
var name = _parameterSegments[i];
if (name != null)
{
yield return (name, i, segments[i]);
}
}
}
}
}
@@ -1,79 +0,0 @@
# MQTTNet.Extensions.TopicTemplate
Provides mqtt topic templating logic to support dispatch, routing and
similar functionality based on the well known moustache syntax, aka
AsyncAPI dynamic channel address.
Generating and parsing MQTT topics and topic filters is often done in
an ad-hoc fashion, leading to error-prone code and subtly introduced
showstoppers like forgetting to strip a slash from a parameter. To
remedy, this extension aids you write safe, maintainable logic
for topic generation, routing and dispatch.
## How it works
The package lets you write MQTTNet code like:
```csharp
var template = new MqttTopicTemplate("App/v1/{sender}/message");
var filter = new MqttTopicFilterBuilder()
.WithTopicTemplate(template) // (1)
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
// filter.Topic == "App/v1/+/message"
var myTopic = template
.WithParameter("sender", "me, myself & i"); // (2)
var message = new MqttApplicationMessageBuilder()
.WithTopicTemplate( // (3)
myTopic)
.WithPayload("Hello!")
.Build();
// message.Topic == "App/v1/me, myself & i/message"
Assert.IsTrue(message.MatchesTopicTemplate(template)); // (4)
```
At (1), the parameter "sender" was automatically replaced by a plus sign
(single level wildcard) because the caller is creating a topic filter.
Using the same template (they are immutable), you can send
a message. At (2), we replace the parameter with a known value.
If that value contained a level separator or topic filter only
characters, we'd get an ArgumentException. If we forgot to replace
the parameter "sender" (e.g. when placing template not myTopic),
we would be prevented from sending at (3).
On the receiving side, a message can be matched to a template
using code like at (4).
These daily tasks become much safer and easier to code than
string.Format() and Split() allow for. More elaborate computation is supported
too. You could extract the sender using the template or route
messages based on topic content.
## Features
- Safe handling of topic parameters in moustache syntax
- Simplifies topic generation and parsing logic
- Match topics and extract the values of their parameters
- Translate between topic templates to ease dynamic routing
- Derive a canonical topic filter from a bunch of templates
- Integrates with MQTTNet message, subscription and filter builders
- Extension method friendly (sealed class with one public ctor)
## Additional documentation
- Some of the [unit tests](https://github.com/dotnet/MQTTnet/blob/master/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs) are instructive
- Some examples feature the extension, e.g. TopicGenerator, Client_Subscribe_Samples
## Feedback
Report any issues with the main project on https://github.com/dotnet/MQTTnet/issues
@@ -1,190 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using MQTTnet.Client;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet.Extensions.TopicTemplate
{
public static class TopicTemplateExtensions
{
/// <summary>
/// Modify this message builder to respond to a given message. The
/// message's response topic and correlation data are included
/// in the message builder.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="BuildResponse" />
public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
{
if (!string.IsNullOrEmpty(message.ResponseTopic))
{
throw new ArgumentException("message does not have a response topic");
}
return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
}
/// <summary>
/// Set the filter topic according to the template, with
/// remaining template parameters substituted by single-level
/// wildcard.
/// </summary>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
}
/// <summary>
/// Create a message builder from this template. The template must not have
/// remaining parameters.
/// </summary>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>a new message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
{
return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
}
/// <summary>
/// Return a message builder to respond to this message. The
/// message's response topic and correlation data are included
/// in the response message builder.
/// </summary>
/// <param
/// name="message">
/// a message with a response topic
/// </param>
/// <returns>a message builder</returns>
/// <see
/// cref="AsResponseTo" />
public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
{
return new MqttApplicationMessageBuilder().AsResponseTo(message);
}
/// <summary>
/// Return whether the message matches the given topic template.
/// </summary>
/// <param
/// name="message">
/// a message
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subtree">
/// whether to include the topic subtree
/// </param>
/// <returns></returns>
public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
{
return topicTemplate.MatchesTopic(message.Topic, subtree);
}
/// <summary>
/// Set the filter topic according to the template, with
/// template parameters substituted by a single-level
/// wildcard.
/// </summary>
/// <param
/// name="builder">
/// a filter builder
/// </param>
/// <param
/// name="topicTemplate">
/// a topic template
/// </param>
/// <param
/// name="subscribeTreeRoot">
/// whether to subscribe to the whole topic tree
/// </param>
/// <returns>the modified topic filter</returns>
public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
{
return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
}
/// <summary>
/// Set the subscription to the template's topic filter.
/// </summary>
/// <returns>the builder</returns>
public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
this MqttClientSubscribeOptionsBuilder builder,
MqttTopicTemplate topicTemplate,
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
bool noLocal = false,
bool retainAsPublished = false,
MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
{
return builder.WithTopicFilter(
new MqttTopicFilter
{
Topic = topicTemplate.TopicFilter,
QualityOfServiceLevel = qualityOfServiceLevel,
NoLocal = noLocal,
RetainAsPublished = retainAsPublished,
RetainHandling = retainHandling
});
}
/// <summary>
/// Set the publication topic according to the topic template. The template
/// must not have remaining (unset) parameters or contain wildcards.
/// </summary>
/// <param
/// name="builder">
/// a message builder
/// </param>
/// <param
/// name="topicTemplate">
/// a parameterless topic template
/// </param>
/// <returns>the modified message builder</returns>
/// <exception
/// cref="ArgumentException">
/// if the topic template has parameters
/// </exception>
public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
{
if (topicTemplate.Parameters.Any())
{
throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
}
MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
return builder.WithTopic(topicTemplate.Template);
}
}
}
@@ -76,7 +76,7 @@ namespace MQTTnet.Extensions.WebSocket4Net
List<KeyValuePair<string, string>> customHeaders = null;
if (_webSocketOptions.RequestHeaders != null)
{
customHeaders = _webSocketOptions.RequestHeaders.ToList();
customHeaders = _webSocketOptions.RequestHeaders.Select(i => new KeyValuePair<string, string>(i.Key, i.Value)).ToList();
}
EndPoint proxy = null;
@@ -21,7 +21,6 @@
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
</ItemGroup>
</Project>
+4 -18
View File
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using MQTTnet.Extensions.TopicTemplate;
using System.Text;
namespace MQTTnet.TestApp
{
@@ -17,8 +17,6 @@ namespace MQTTnet.TestApp
singleWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
multiWildcardTopicsByPublisher = new Dictionary<string, List<string>>();
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
// Find some reasonable distribution across three topic levels
var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0));
@@ -42,37 +40,25 @@ namespace MQTTnet.TestApp
int publisherTopicCount = 0;
var publisherName = "pub" + p;
var publisherTemplate = baseTemplate
.WithParameter("publisher", publisherName);
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
{
var l1Template = publisherTemplate
.WithParameter("building", "building" + (l1 + 1));
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
{
var l2Template = l1Template
.WithParameter("level", "level" + (l2 + 1));
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
{
if (publisherTopicCount >= numTopicsPerPublisher)
break;
var topic = l2Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1);
AddPublisherTopic(publisherName, topic, topicsByPublisher);
if (l2 == 0)
{
var singleWildcardTopic = l1Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1);
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
if (l1 == 0)
{
var multiWildcardTopic = publisherTemplate
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l3 + 1);
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
}
}
@@ -13,7 +13,6 @@ using MQTTnet.Diagnostics;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
using MQTTnet.Tests.Mockups;
@@ -575,154 +574,16 @@ namespace MQTTnet.Tests.Clients.ManagedMqttClient
}
}
[TestMethod]
public async Task Subscribe_Does_Not_Hang_On_Server_Stop()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_2";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new SemaphoreSlim(0, 1);
var failedOnClient = new SemaphoreSlim(0, 1);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttSubscribePacket)
{
if (reject)
{
e.ProcessPacket = false;
}
receivedOnServer.Release();
}
return CompletedTask.Instance;
};
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
{
failedOnClient.Release();
return CompletedTask.Instance;
};
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
reject = false;
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
}
}
[TestMethod]
public async Task Unsubscribe_Does_Not_Hang_On_Server_Stop()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_2";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new SemaphoreSlim(0, 1);
var failedOnClient = new SemaphoreSlim(0, 1);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttUnsubscribePacket)
{
if (reject)
{
e.ProcessPacket = false;
}
receivedOnServer.Release();
}
else if (e.Packet is MqttSubscribePacket)
{
receivedOnServer.Release();
}
return CompletedTask.Instance;
};
managedClient.SynchronizingSubscriptionsFailedAsync += e =>
{
failedOnClient.Release();
return CompletedTask.Instance;
};
await managedClient.SubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
await managedClient.UnsubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
Assert.IsTrue(await failedOnClient.WaitAsync(testTimeout));
reject = false;
await managedClient.UnsubscribeAsync(topic);
Assert.IsTrue(await receivedOnServer.WaitAsync(testTimeout));
}
}
[TestMethod]
public async Task Publish_Does_Not_Hang_On_Server_Error()
{
var timeout = TimeSpan.FromSeconds(2);
var testTimeout = TimeSpan.FromSeconds(timeout.TotalSeconds * 2);
const string topic = "test_topic_42";
using (var testEnvironment = CreateTestEnvironment())
using (var managedClient = await CreateManagedClientAsync(testEnvironment, timeout: timeout))
{
testEnvironment.IgnoreClientLogErrors = true;
bool reject = true;
var receivedOnServer = new TaskCompletionSource<object>();
managedClient.ApplicationMessageProcessedAsync += e => Task.FromResult(reject &= e.Exception is null);
testEnvironment.Server.InterceptingInboundPacketAsync += e =>
{
if (e.Packet is MqttPublishPacket)
{
if (reject)
{
e.ProcessPacket = false;
}
else
{
receivedOnServer.TrySetResult(null);
}
}
return CompletedTask.Instance;
};
await managedClient.EnqueueAsync(new MqttApplicationMessage { Topic = topic, PayloadSegment = new ArraySegment<byte>(new byte[] { 1 }), Retain = true, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce });
var timeoutTask = Task.Delay(testTimeout);
var firstDone = await Task.WhenAny(receivedOnServer.Task, timeoutTask);
Assert.AreEqual(receivedOnServer.Task, firstDone, "Client is hung on publish!");
}
}
async Task<MQTTnet.Extensions.ManagedClient.ManagedMqttClient> CreateManagedClientAsync(
TestEnvironment testEnvironment,
IMqttClient underlyingClient = null,
TimeSpan? connectionCheckInterval = null,
string host = "localhost",
TimeSpan? timeout = null)
string host = "localhost")
{
await testEnvironment.StartServer();
var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort);
if (timeout != null)
{
clientOptions.WithTimeout(timeout.Value);
}
var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build();
// Use a short connection check interval so that subscription operations are performed quickly
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -66,7 +65,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
return CompletedTask.Instance;
};
await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build());
await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build());
Assert.Fail("Must fail!");
}
@@ -168,91 +167,6 @@ namespace MQTTnet.Tests.Clients.MqttClient
}
}
class TestClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler
{
public static IReadOnlyList<string> ServerTokens { get; } = new List<string>()
{
string.Empty,
"reply context token"
};
public static IReadOnlyList<string> ClientTokens { get; } = new List<string>()
{
string.Empty,
"initial context token",
string.Empty
};
int tokenCursor;
public async Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context)
{
if (context.AuthenticationMethod != "GS2-KRB5")
{
throw new InvalidOperationException("Wrong authentication method");
}
Assert.IsTrue(tokenCursor < ServerTokens.Count, "No more server tokens were expected.");
var expectedToken = ServerTokens[tokenCursor++];
Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(context.AuthenticationData));
var sendOptions = new MqttExtendedAuthenticationExchangeData
{
AuthenticationData = Encoding.UTF8.GetBytes(ClientTokens[tokenCursor])
};
await context.Client.SendExtendedAuthenticationExchangeDataAsync(sendOptions, context.CancellationToken);
}
}
[TestMethod]
public async Task Use_Extended_Authentication()
{
// Use Kerberos sample from the MQTT RFC.
var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler();
using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500))
{
var server = await testEnvironment.StartServer();
server.ValidatingConnectionAsync += async args =>
{
if (args.AuthenticationMethod == "GS2-KRB5")
{
var authenticationData = args.AuthenticationData;
string expectedToken;
int tokenCursor;
for (tokenCursor = 0; tokenCursor < TestClientKerberosAuthenticationHandler.ServerTokens.Count; tokenCursor++)
{
expectedToken = TestClientKerberosAuthenticationHandler.ClientTokens[tokenCursor];
Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(authenticationData), "The received client token is not correct.");
var serverToken = TestClientKerberosAuthenticationHandler.ServerTokens[tokenCursor];
var authOptions = testEnvironment.Factory.CreateMqttExtendedAuthenticationExchangeOptionsBuilder().WithAuthenticationData(serverToken).Build();
var result = await args.SendExtendedAuthenticationExchangeDataAsync(authOptions);
authenticationData = result.AuthenticationData;
}
expectedToken = TestClientKerberosAuthenticationHandler.ClientTokens[tokenCursor];
Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(authenticationData), "The received client token is not correct.");
args.ResponseAuthenticationData = Encoding.UTF8.GetBytes("outcome of authentication");
args.ReasonCode = MqttConnectReasonCode.Success;
args.ReasonString = "Authentication successful";
}
else
{
args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod;
}
};
var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithAuthentication("GS2-KRB5", new byte[0]).WithExtendedAuthenticationExchangeHandler(kerberosAuthenticationHandler);
var client = await testEnvironment.ConnectClient(clientOptions);
Assert.IsTrue(client.IsConnected);
}
}
[TestMethod]
public async Task No_Unobserved_Exception()
{
@@ -261,7 +175,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
testEnvironment.IgnoreClientLogErrors = true;
var client = testEnvironment.CreateClient();
var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build();
var options = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").WithTimeout(TimeSpan.FromSeconds(2)).Build();
try
{
@@ -326,9 +240,9 @@ namespace MQTTnet.Tests.Clients.MqttClient
await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce);
}
}
catch (MqttClientNotConnectedException exception)
catch (MqttCommunicationException exception)
{
if (exception.Message == "The MQTT client is not connected.")
if (exception.Message == "The client is not connected.")
{
return;
}
@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
@@ -87,7 +86,7 @@ namespace MQTTnet.Tests.Clients.MqttClient
await client.ConnectAsync(clientOptions);
}
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task Connect_Multiple_Times_Should_Fail()
@@ -749,8 +748,8 @@ namespace MQTTnet.Tests.Clients.MqttClient
{
if (e.ApplicationMessage.Topic == "request")
{
// Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for
// the ACK etc. The problem is that the SpinUntil below only waits until the
// Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for
// the ACK etc. The problem is that the SpinUntil below only waits until the
// flag is set. It does not wait until the client has sent the ACK
await client2.PublishStringAsync("reply");
}
@@ -926,54 +925,5 @@ namespace MQTTnet.Tests.Clients.MqttClient
Assert.IsFalse(disconnectedFired);
}
}
[TestMethod]
public void Backward_compatible_TCP_options()
{
var options = new MqttClientOptionsBuilder().WithTcpServer("host", 3).Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptions
{
ChannelOptions = new MqttClientTcpOptions
{
Server = "host",
Port = 3
}
};
Assert.AreEqual("host:3", options.ChannelOptions.ToString());
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptionsBuilder().WithEndPoint(new DnsEndPoint("host", 3)).Build();
Assert.AreEqual("Unspecified/host:3", options.ChannelOptions.ToString());
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(3, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptionsBuilder().WithTcpServer("host").Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(1883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
Assert.AreEqual("Unspecified/host:1883", options.ChannelOptions.ToString());
options = new MqttClientOptionsBuilder().WithTlsOptions(o => o.UseTls()).WithTcpServer("host").Build();
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(8883, ((MqttClientTcpOptions)options.ChannelOptions).Port);
options = new MqttClientOptions
{
ChannelOptions = new MqttClientTcpOptions
{
Server = "host"
}
};
Assert.AreEqual("host", ((MqttClientTcpOptions)options.ChannelOptions).Server);
Assert.AreEqual(null, ((MqttClientTcpOptions)options.ChannelOptions).Port);
}
}
}
@@ -1,273 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Exceptions;
using MQTTnet.Extensions.TopicTemplate;
namespace MQTTnet.Tests.Extensions
{
[TestClass]
public sealed class MqttTopicTemplate_Tests
{
[TestMethod]
public void BasicOps()
{
var template = new MqttTopicTemplate("A/B/{param}/D");
Assert.AreEqual("A/B/C/D", template.WithParameter("param", "C").Template);
Assert.AreEqual("A/B/+/D", template.WithoutParameter("param").Template);
Assert.AreEqual("A/B/+/D", template.TopicFilter);
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
// corner cases
Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter);
Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter);
Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter);
}
[TestMethod]
public void BasicOpsWithHash()
{
var template = new MqttTopicTemplate("A/B/{param}/D/#");
Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "C").Template);
Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param").Template);
Assert.AreEqual("A/B/+/D/#", template.TopicFilter);
Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars1()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a#");
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars2()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a+b");
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void RejectsReservedChars3()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a/b");
}
[TestMethod]
public void AcceptsEmptyValue()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "");
}
[TestMethod]
[ExpectedException(typeof(MqttProtocolViolationException))]
public void RejectsEmptyTemplate()
{
var _ = new MqttTopicTemplate("");
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void RejectsNullTemplate()
{
var _ = new MqttTopicTemplate(null);
}
[TestMethod]
public void IgnoresEmptyParameters()
{
var template = new MqttTopicTemplate("A/B/{}/D");
Assert.IsFalse(template.Parameters.Any());
}
[TestMethod]
public void AcceptsValidTopics()
{
_ = new MqttTopicTemplate("A");
_ = new MqttTopicTemplate("/");
_ = new MqttTopicTemplate("/A");
_ = new MqttTopicTemplate("A/#");
_ = new MqttTopicTemplate("$SYS/#");
_ = new MqttTopicTemplate("////");
_ = new MqttTopicTemplate("A/B/{foo}/D/");
}
[TestMethod]
public void DynamicRoutingSupport()
{
var v1template = new MqttTopicTemplate("A/v1/{id}/F");
var v2template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}");
var incoming_v1_topic = "A/v1/32/F";
var compat_v2_topic = v2template
.WithParameterValuesFrom(
v1template.ParseParameterValues(incoming_v1_topic))
.WithParameter("bar", "unknown");
// id: 32 was relocated and bar: unknown added
Assert.AreEqual("A/v2/foo/32/unknown", compat_v2_topic.Template);
}
[TestMethod]
public void SubscriptionSupport()
{
var template = new MqttTopicTemplate("A/v1/{param}/F");
var filter = template.BuildFilter()
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
Assert.AreEqual("A/v1/+/F", filter.Topic);
}
[TestMethod]
public void SubscriptionSupport2()
{
var template = new MqttTopicTemplate("A/v1/{param}/F");
var subscribeOptions = new MqttFactory().CreateSubscribeOptionsBuilder()
.WithTopicTemplate(template)
.WithSubscriptionIdentifier(5)
.Build();
Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic);
}
[TestMethod]
public void SendAndSubscribeSupport()
{
var template = new MqttTopicTemplate("App/v1/{sender}/message");
var filter = new MqttTopicFilterBuilder()
.WithTopicTemplate(template)
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
Assert.AreEqual("App/v1/+/message", filter.Topic);
var myTopic = template.WithParameter("sender", "me, myself & i");
var message = new MqttApplicationMessageBuilder()
.WithTopicTemplate(
myTopic)
.WithPayload("Hello!")
.Build();
Assert.IsTrue(message.MatchesTopicTemplate(template));
}
[TestMethod]
public void SendAndSubscribeSupport2()
{
var template = new MqttTopicTemplate("App/v1/{sender}/message");
Assert.ThrowsException<ArgumentException>(() =>
template.BuildMessage());
}
[TestMethod]
public void CanonicalPrefixFilter()
{
var template1 = new MqttTopicTemplate("A/v1/{param}/F");
var template2 = new MqttTopicTemplate("A/v1/E/F");
var template3 = new MqttTopicTemplate("A/v1/E/F/G/H");
var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3 });
// possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
var template2b = new MqttTopicTemplate("A/v1/E/X");
canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template2b });
Assert.AreEqual("A/v1/+", canonicalFilter.Template);
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
var template4 = new MqttTopicTemplate("A/v2/{prefix}");
var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template4 });
Assert.AreEqual("A/+", canonicalFilter2.TopicFilter);
Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple1()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A"),
new MqttTopicTemplate("B")
});
Assert.AreEqual("#", template.TopicFilter);
Assert.AreEqual("#", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple2()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/F/"),
new MqttTopicTemplate("A/v1/E/X")
});
Assert.AreEqual("A/v1/+", template.TopicFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple3()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/F"),
new MqttTopicTemplate("A/v1/{param}/F/X")
});
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple4()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/{param}/FFA"),
new MqttTopicTemplate("A/v1/{param}/FX")
});
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
[TestMethod]
public void CanonicalPrefixFilterSimple5()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/v1/E/F/G/H"),
new MqttTopicTemplate("A/v2/{param}")
});
Assert.AreEqual("A/+", template.TopicFilter);
Assert.AreEqual("A/+/#", template.TopicTreeRootFilter);
}
[TestMethod]
public void CanonicalPrefixFilterSimple6()
{
var template = MqttTopicTemplate.FindCanonicalPrefix(new[]
{
new MqttTopicTemplate("A/{version}/E/F/G/H"),
new MqttTopicTemplate("A/{version}/{param}")
});
Assert.AreEqual("A/+/+", template.TopicFilter);
Assert.AreEqual("A/+/+/#", template.TopicTreeRootFilter);
}
}
}
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -18,7 +17,7 @@ namespace MQTTnet.Tests.Internal
[TestMethod]
public async Task Connect_Send_Receive()
{
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
var crossPlatformSocket = new CrossPlatformSocket();
await crossPlatformSocket.ConnectAsync("www.google.de", 80, CancellationToken.None);
var requestBuffer = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nHost: www.google.de\r\n\r\n");
@@ -37,7 +36,7 @@ namespace MQTTnet.Tests.Internal
[ExpectedException(typeof(OperationCanceledException))]
public async Task Try_Connect_Invalid_Host()
{
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
var crossPlatformSocket = new CrossPlatformSocket();
var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5));
cancellationToken.Token.Register(() => crossPlatformSocket.Dispose());
@@ -66,7 +65,7 @@ namespace MQTTnet.Tests.Internal
[TestMethod]
public void Set_Options()
{
var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp);
var crossPlatformSocket = new CrossPlatformSocket();
Assert.IsFalse(crossPlatformSocket.ReuseAddress);
crossPlatformSocket.ReuseAddress = true;
@@ -21,7 +21,6 @@
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj" />
<ProjectReference Include="..\..\Source\MQTTnet\MQTTnet.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
<ProjectReference Include="..\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj" />
</ItemGroup>
+53 -59
View File
@@ -2,13 +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 Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Implementations;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Implementations;
namespace MQTTnet.Tests
{
@@ -18,68 +18,62 @@ namespace MQTTnet.Tests
[TestMethod]
public async Task Dispose_Channel_While_Used()
{
using (var ct = new CancellationTokenSource())
var ct = new CancellationTokenSource();
var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
try
{
using (var serverSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp))
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
serverSocket.Listen(0);
#pragma warning disable 4014
Task.Run(async () =>
#pragma warning restore 4014
{
try
while (!ct.IsCancellationRequested)
{
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001));
serverSocket.Listen(0);
#pragma warning disable 4014
Task.Run(
async () =>
#pragma warning restore 4014
{
while (!ct.IsCancellationRequested)
{
var client = await serverSocket.AcceptAsync(ct.Token);
var data = new byte[] { 128 };
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
}
},
ct.Token);
var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
await Task.Delay(100, ct.Token);
var buffer = new byte[1];
await tcpChannel.ReadAsync(buffer, 0, 1, ct.Token);
Assert.AreEqual(128, buffer[0]);
// This block should fail after dispose.
#pragma warning disable 4014
Task.Run(
() =>
#pragma warning restore 4014
{
Task.Delay(200, ct.Token);
tcpChannel.Dispose();
},
ct.Token);
try
{
await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None);
}
catch (Exception exception)
{
Assert.IsInstanceOfType(exception, typeof(SocketException));
Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode);
}
}
finally
{
ct.Cancel(false);
var client = await serverSocket.AcceptAsync();
var data = new byte[] { 128 };
await client.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);
}
}, ct.Token);
var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork);
await clientSocket.ConnectAsync("localhost", 50001, CancellationToken.None);
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), "test", null);
await Task.Delay(100, ct.Token);
var buffer = new byte[1];
await tcpChannel.ReadAsync(buffer, 0, 1, ct.Token);
Assert.AreEqual(128, buffer[0]);
// This block should fail after dispose.
#pragma warning disable 4014
Task.Run(() =>
#pragma warning restore 4014
{
Task.Delay(200, ct.Token);
tcpChannel.Dispose();
}, ct.Token);
try
{
await tcpChannel.ReadAsync(buffer, 0, 1, CancellationToken.None);
}
catch (Exception exception)
{
Assert.IsInstanceOfType(exception, typeof(SocketException));
Assert.AreEqual(SocketError.OperationAborted, ((SocketException)exception).SocketErrorCode);
}
}
finally
{
ct.Cancel(false);
serverSocket.Dispose();
}
}
}
}
}
@@ -23,7 +23,7 @@ namespace MQTTnet.Tests.Server
{
await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1)));
var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
var client = new CrossPlatformSocket(AddressFamily.InterNetwork);
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
// Don't send anything. The server should close the connection.
@@ -54,7 +54,7 @@ namespace MQTTnet.Tests.Server
// Send an invalid packet and ensure that the server will close the connection and stay in a waiting state
// forever. This is security related.
var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
var client = new CrossPlatformSocket(AddressFamily.InterNetwork);
await client.ConnectAsync("localhost", testEnvironment.ServerPort, CancellationToken.None);
var buffer = Encoding.UTF8.GetBytes("Garbage");
+4 -17
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using MQTTnet.Extensions.TopicTemplate;
namespace MQTTnet.Tests
{
@@ -32,43 +31,31 @@ namespace MQTTnet.Tests
{
maxNumLevel3Topics = 1;
}
MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}");
for (var p = 0; p < numPublishers; ++p)
{
int publisherTopicCount = 0;
var publisherName = "pub" + p;
var publisherTemplate = baseTemplate
.WithParameter("publisher", publisherName);
for (var l1 = 0; l1 < numLevel1Topics; ++l1)
{
var l1Template = publisherTemplate.WithParameter("building", "building" + (l1 + 1));
for (var l2 = 0; l2 < numLevel2Topics; ++l2)
{
var l2Template = l1Template.WithParameter("level", "level" + (l2 + 1));
for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3)
{
if (publisherTopicCount >= numTopicsPerPublisher)
break;
var l3Template = l2Template
.WithParameter("sensor", "sensor" + (l3 + 1));
AddPublisherTopic(publisherName, l3Template.TopicFilter, topicsByPublisher);
var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1);
AddPublisherTopic(publisherName, topic, topicsByPublisher);
if (l2 == 0)
{
var singleWildcardTopic = l1Template
.WithParameter("sensor", "sensor" + (l3 + 1))
.TopicFilter;
var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1);
AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher);
}
if ((l1 == 0) && (l3 == 0))
{
var multiWildcardTopic = publisherTemplate
.WithParameter("level", "level" + (l2 + 1))
.TopicFilter;
var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l2 + 1);
AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher);
}
+3 -12
View File
@@ -141,8 +141,7 @@ namespace MQTTnet.Adapter
try
{
var localPacketInspector = PacketInspector;
localPacketInspector?.BeginReceivePacket();
PacketInspector?.BeginReceivePacket();
ReceivedMqttPacket receivedPacket;
var receivedPacketTask = ReceiveAsync(cancellationToken);
@@ -160,10 +159,7 @@ namespace MQTTnet.Adapter
return null;
}
if (localPacketInspector != null)
{
await localPacketInspector.EndReceivePacket().ConfigureAwait(false);
}
PacketInspector?.EndReceivePacket();
Interlocked.Add(ref _statistics._bytesSent, receivedPacket.TotalLength);
@@ -219,13 +215,8 @@ namespace MQTTnet.Adapter
try
{
var packetBuffer = PacketFormatterAdapter.Encode(packet);
PacketInspector?.BeginSendPacket(packetBuffer);
var localPacketInspector = PacketInspector;
if (localPacketInspector != null)
{
await localPacketInspector.BeginSendPacket(packetBuffer).ConfigureAwait(false);
}
_logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet);
if (packetBuffer.Payload.Count == 0 || !AllowPacketFragmentation)
+25 -30
View File
@@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using MQTTnet.Diagnostics;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -13,20 +12,16 @@ namespace MQTTnet.Adapter
{
public sealed class MqttPacketInspector
{
readonly AsyncEvent<InspectMqttPacketEventArgs> _asyncEvent;
readonly MqttNetSourceLogger _logger;
readonly AsyncEvent<InspectMqttPacketEventArgs> _asyncEvent;
MemoryStream _receivedPacketBuffer;
public MqttPacketInspector(AsyncEvent<InspectMqttPacketEventArgs> asyncEvent, IMqttNetLogger logger)
{
_asyncEvent = asyncEvent ?? throw new ArgumentNullException(nameof(asyncEvent));
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (logger == null) throw new ArgumentNullException(nameof(logger));
_logger = logger.WithSource(nameof(MqttPacketInspector));
}
@@ -41,15 +36,28 @@ namespace MQTTnet.Adapter
{
_receivedPacketBuffer = new MemoryStream();
}
_receivedPacketBuffer?.SetLength(0);
}
public Task BeginSendPacket(MqttPacketBuffer buffer)
public void EndReceivePacket()
{
if (!_asyncEvent.HasHandlers)
{
return CompletedTask.Instance;
return;
}
var buffer = _receivedPacketBuffer.ToArray();
_receivedPacketBuffer.SetLength(0);
InspectPacket(buffer, MqttPacketFlowDirection.Inbound);
}
public void BeginSendPacket(MqttPacketBuffer buffer)
{
if (!_asyncEvent.HasHandlers)
{
return;
}
// Create a copy of the actual packet so that the inspector gets no access
@@ -57,20 +65,7 @@ namespace MQTTnet.Adapter
// intended for debugging etc. so that this is OK.
var bufferCopy = buffer.ToArray();
return InspectPacket(bufferCopy, MqttPacketFlowDirection.Outbound);
}
public Task EndReceivePacket()
{
if (!_asyncEvent.HasHandlers)
{
return CompletedTask.Instance;
}
var buffer = _receivedPacketBuffer.ToArray();
_receivedPacketBuffer.SetLength(0);
return InspectPacket(buffer, MqttPacketFlowDirection.Inbound);
InspectPacket(bufferCopy, MqttPacketFlowDirection.Outbound);
}
public void FillReceiveBuffer(byte[] buffer)
@@ -79,16 +74,16 @@ namespace MQTTnet.Adapter
{
return;
}
_receivedPacketBuffer?.Write(buffer, 0, buffer.Length);
}
async Task InspectPacket(byte[] buffer, MqttPacketFlowDirection direction)
void InspectPacket(byte[] buffer, MqttPacketFlowDirection direction)
{
try
{
var eventArgs = new InspectMqttPacketEventArgs(direction, buffer);
await _asyncEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
_asyncEvent.InvokeAsync(eventArgs).GetAwaiter().GetResult();
}
catch (Exception exception)
{
@@ -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;
using System.Collections.Generic;
using MQTTnet.Exceptions;
using MQTTnet.Packets;
@@ -12,9 +11,8 @@ namespace MQTTnet.Client
{
public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException
{
public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket, Exception innerExcpetion = null) : base(
$"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.",
innerExcpetion)
public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket)
: base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.")
{
ReasonCode = disconnectPacket.ReasonCode;
SessionExpiryInterval = disconnectPacket.SessionExpiryInterval;
@@ -25,12 +23,12 @@ namespace MQTTnet.Client
public MqttDisconnectReasonCode? ReasonCode { get; }
public string ReasonString { get; }
public string ServerReference { get; }
public uint? SessionExpiryInterval { get; }
public string ReasonString { get; }
public List<MqttUserProperty> UserProperties { get; }
public string ServerReference { get; }
}
}
}
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -12,7 +11,7 @@ namespace MQTTnet.Client
{
public class MqttExtendedAuthenticationExchangeContext
{
public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client, CancellationToken cancellationToken)
public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client)
{
if (authPacket == null) throw new ArgumentNullException(nameof(authPacket));
@@ -23,7 +22,6 @@ namespace MQTTnet.Client
UserProperties = authPacket.UserProperties;
Client = client ?? throw new ArgumentNullException(nameof(client));
CancellationToken = cancellationToken;
}
/// <summary>
@@ -60,10 +58,5 @@ namespace MQTTnet.Client
public List<MqttUserProperty> UserProperties { get; }
public MqttClient Client { get; }
/// <summary>
/// The cancellation token passed to the operation being authenticated (e.g. ConnectAsync).
/// </summary>
public CancellationToken CancellationToken { get; }
}
}
+119 -188
View File
@@ -48,7 +48,6 @@ namespace MQTTnet.Client
Task _packetReceiverTask;
AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue;
Task _publishPacketReceiverTask;
MqttDisconnectPacket _unexpectedDisconnectPacket;
public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger)
{
@@ -89,8 +88,6 @@ namespace MQTTnet.Client
public bool IsConnected => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connected;
bool IsConnecting => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connecting;
public MqttClientOptions Options { get; private set; }
public async Task<MqttClientConnectResult> ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken = default)
@@ -126,8 +123,6 @@ namespace MQTTnet.Client
var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_events.InspectPacketEvent, _rootLogger), _rootLogger);
_adapter = adapter ?? throw new InvalidOperationException("The adapter factory did not provide an adapter.");
_unexpectedDisconnectPacket = null;
if (cancellationToken.CanBeCanceled)
{
connectResult = await ConnectInternal(adapter, cancellationToken).ConfigureAwait(false);
@@ -220,7 +215,7 @@ namespace MQTTnet.Client
}
// Sending the DISCONNECT may fail due to connection issues. The resulting exception
// must be thrown to let the caller know that the disconnect was not clean.
// must be throw to let the caller know that the disconnect is not a clean one.
var disconnectPacket = MqttPacketFactories.Disconnect.Create(options);
if (cancellationToken.CanBeCanceled)
@@ -244,7 +239,7 @@ namespace MQTTnet.Client
public async Task PingAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
ThrowIfNotConnected();
@@ -306,7 +301,7 @@ namespace MQTTnet.Client
}
ThrowIfDisposed();
ThrowIfNotConnectedOrConnecting();
ThrowIfNotConnected();
var authPacket = new MqttAuthPacket
{
@@ -335,7 +330,7 @@ namespace MQTTnet.Client
ThrowIfDisposed();
ThrowIfNotConnected();
if (Options.ValidateFeatures)
{
MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion);
@@ -411,38 +406,30 @@ namespace MQTTnet.Client
Task AcknowledgeReceivedPublishPacket(MqttApplicationMessageReceivedEventArgs eventArgs, CancellationToken cancellationToken)
{
switch (eventArgs.PublishPacket.QualityOfServiceLevel)
if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
{
case MqttQualityOfServiceLevel.AtMostOnce:
// no response required
}
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
{
if (!eventArgs.ProcessingFailed)
{
// no response required
break;
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs);
return Send(pubAckPacket, cancellationToken);
}
case MqttQualityOfServiceLevel.AtLeastOnce:
}
else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
{
if (!eventArgs.ProcessingFailed)
{
if (!eventArgs.ProcessingFailed)
{
var pubAckPacket = MqttPacketFactories.PubAck.Create(eventArgs);
return Send(pubAckPacket, cancellationToken);
}
break;
}
case MqttQualityOfServiceLevel.ExactlyOnce:
{
if (!eventArgs.ProcessingFailed)
{
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs);
return Send(pubRecPacket, cancellationToken);
}
break;
}
default:
{
throw new MqttProtocolViolationException("Received a not supported QoS level.");
var pubRecPacket = MqttPacketFactories.PubRec.Create(eventArgs);
return Send(pubRecPacket, cancellationToken);
}
}
else
{
throw new MqttProtocolViolationException("Received a not supported QoS level.");
}
return CompletedTask.Instance;
}
@@ -456,31 +443,24 @@ namespace MQTTnet.Client
var connectPacket = MqttPacketFactories.Connect.Create(options);
await Send(connectPacket, cancellationToken).ConfigureAwait(false);
for (; ; )
var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false);
if (receivedPacket is MqttConnAckPacket connAckPacket)
{
result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion);
}
else if (receivedPacket is MqttAuthPacket)
{
throw new NotSupportedException("Extended authentication handler is not yet supported");
}
else if (receivedPacket == null)
{
cancellationToken.ThrowIfCancellationRequested();
var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false);
if (receivedPacket is MqttAuthPacket authPacket)
{
await ProcessReceivedAuthPacket(authPacket, cancellationToken).ConfigureAwait(false);
continue;
}
if (receivedPacket is MqttConnAckPacket connAckPacket)
{
result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion);
break;
}
if (receivedPacket != null)
{
throw new MqttProtocolViolationException($"Received other packet than CONNACK or AUTH while connecting ({receivedPacket}).");
}
throw new MqttCommunicationException("Connection closed.");
}
else
{
throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket}).");
}
}
catch (Exception exception)
{
@@ -609,8 +589,8 @@ namespace MQTTnet.Client
_disconnectUserProperties,
exception);
// This handler must be executed in a new thread to prevent a deadlock
// that may occur when attempting to reconnect within that handler, etc.
// This handler must be executed in a new thread because otherwise a dead lock may happen
// when trying to reconnect in that handler etc.
Task.Run(() => _events.DisconnectedEvent.InvokeAsync(eventArgs)).RunInBackground(_logger);
}
}
@@ -619,9 +599,12 @@ namespace MQTTnet.Client
{
var clientWasConnected = IsConnected;
return !DisconnectIsPendingOrFinished() ?
DisconnectCore(sender, exception, connectResult, clientWasConnected)
: CompletedTask.Instance;
if (!DisconnectIsPendingOrFinished())
{
return DisconnectCore(sender, exception, connectResult, clientWasConnected);
}
return CompletedTask.Instance;
}
bool DisconnectIsPendingOrFinished()
@@ -674,46 +657,24 @@ namespace MQTTnet.Client
Task OnConnected(MqttClientConnectResult connectResult)
{
if (!_events.ConnectedEvent.HasHandlers)
if (_events.ConnectedEvent.HasHandlers)
{
return CompletedTask.Instance;
var eventArgs = new MqttClientConnectedEventArgs(connectResult);
return _events.ConnectedEvent.InvokeAsync(eventArgs);
}
var eventArgs = new MqttClientConnectedEventArgs(connectResult);
return _events.ConnectedEvent.InvokeAsync(eventArgs);
return CompletedTask.Instance;
}
Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket, CancellationToken cancellationToken)
Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket)
{
var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler;
if (extendedAuthenticationExchangeHandler == null)
if (extendedAuthenticationExchangeHandler != null)
{
// From RFC:
// The Client can close the connection at any point in this process. It MAY send a DISCONNECT packet before doing so.
// The Server can reject the authentication at any point in this process. It MAY send a CONNACK with a Reason Code of
// 0x80 or above as described in section 4.13, and MUST close the Network Connection [MQTT-4.12.0-4].
//
// If the re-authentication fails, the Client or Server SHOULD send DISCONNECT with an appropriate Reason Code
// as described in section 4.13, and MUST close the Network Connection [MQTT-4.12.1-2].
//
// Since we have no handler there is no chance to fulfil the initial authentication or re-authentication requests.
if (IsConnecting)
{
throw new MqttCommunicationException("No handler was provided to handle the extended authentication exchange.");
}
_ = DisconnectAsync(new MqttClientDisconnectOptions
{
Reason = MqttClientDisconnectOptionsReason.ImplementationSpecificError,
ReasonString = "Unable to handle AUTH packet"
},
cancellationToken);
return CompletedTask.Instance;
return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this));
}
return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this, cancellationToken));
return CompletedTask.Instance;
}
Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket)
@@ -721,10 +682,9 @@ namespace MQTTnet.Client
_disconnectReason = (int)disconnectPacket.ReasonCode;
_disconnectReasonString = disconnectPacket.ReasonString;
_disconnectUserProperties = disconnectPacket.UserProperties;
_unexpectedDisconnectPacket = disconnectPacket;
// Also dispatch disconnect to waiting threads to generate a proper exception.
_packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket, null));
_packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket));
return DisconnectInternal(_packetReceiverTask, null, null);
}
@@ -764,15 +724,15 @@ namespace MQTTnet.Client
Task ProcessReceivedPubRecPacket(MqttPubRecPacket pubRecPacket, CancellationToken cancellationToken)
{
if (_packetDispatcher.TryDispatch(pubRecPacket))
if (!_packetDispatcher.TryDispatch(pubRecPacket))
{
return CompletedTask.Instance;
// The packet is unknown. Probably due to a restart of the client.
// So wen send this to the server to trigger a full resend of the message.
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
return Send(pubRelPacket, cancellationToken);
}
// The packet is unknown, probably due to a restart of the client.
// So we send this to the server to trigger a full resend of the message.
var pubRelPacket = MqttPacketFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound);
return Send(pubRelPacket, cancellationToken);
return CompletedTask.Instance;
}
Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken)
@@ -791,27 +751,10 @@ namespace MQTTnet.Client
async Task<MqttClientPublishResult> PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
{
try
{
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier]
await Send(publishPacket, cancellationToken).ConfigureAwait(false);
// No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier]
await Send(publishPacket, cancellationToken).ConfigureAwait(false);
return MqttClientResultFactory.PublishResult.Create(null);
}
catch (Exception exception)
{
// We have to check if the server has sent a disconnect packet in response to the published message.
// Since we are in QoS 0 we do not get a direct response via an PUBACK packet and thus basically no
// feedback at all.
var localUnexpectedDisconnectPacket = _unexpectedDisconnectPacket;
if (localUnexpectedDisconnectPacket != null)
{
throw new MqttClientUnexpectedDisconnectReceivedException(localUnexpectedDisconnectPacket, exception);
}
throw;
}
return MqttClientResultFactory.PublishResult.Create(null);
}
async Task<MqttClientPublishResult> PublishExactlyOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
@@ -967,14 +910,6 @@ namespace MQTTnet.Client
}
}
void ThrowIfNotConnectedOrConnecting()
{
if (!(IsConnected || IsConnecting))
{
ThrowNotConnectedOrConnecting();
}
}
static void ThrowIfOptionsInvalid(MqttClientOptions options)
{
if (options == null)
@@ -995,12 +930,7 @@ namespace MQTTnet.Client
static void ThrowNotConnected()
{
throw new MqttClientNotConnectedException();
}
static void ThrowNotConnectedOrConnecting()
{
throw new MqttCommunicationException("The MQTT client is not connected or connecting.");
throw new MqttCommunicationException("The client is not connected.");
}
void TryInitiateDisconnect()
@@ -1022,36 +952,39 @@ namespace MQTTnet.Client
{
try
{
switch (packet)
if (packet is MqttPublishPacket publishPacket)
{
case MqttPublishPacket publishPacket:
EnqueueReceivedPublishPacket(publishPacket);
break;
case MqttPubRecPacket pubRecPacket:
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
break;
case MqttPubRelPacket pubRelPacket:
await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false);
break;
case MqttDisconnectPacket disconnectPacket:
await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false);
break;
case MqttAuthPacket authPacket:
await ProcessReceivedAuthPacket(authPacket, cancellationToken).ConfigureAwait(false);
break;
case MqttPingRespPacket _:
_packetDispatcher.TryDispatch(packet);
break;
case MqttPingReqPacket _:
throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a client to the server only.");
default:
EnqueueReceivedPublishPacket(publishPacket);
}
else if (packet is MqttPubRecPacket pubRecPacket)
{
await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false);
}
else if (packet is MqttPubRelPacket pubRelPacket)
{
await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false);
}
else if (packet is MqttDisconnectPacket disconnectPacket)
{
await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false);
}
else if (packet is MqttAuthPacket authPacket)
{
await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false);
}
else if (packet is MqttPingRespPacket)
{
_packetDispatcher.TryDispatch(packet);
}
else if (packet is MqttPingReqPacket)
{
throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a client to the server only.");
}
else
{
if (!_packetDispatcher.TryDispatch(packet))
{
if (!_packetDispatcher.TryDispatch(packet))
{
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time.");
}
break;
throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time.");
}
}
}
@@ -1062,19 +995,19 @@ namespace MQTTnet.Client
return;
}
switch (exception)
if (exception is OperationCanceledException)
{
case OperationCanceledException _:
break;
case MqttCommunicationException _:
_logger.Warning(exception, "Communication error while receiving packets");
break;
default:
_logger.Error(exception, "Error while processing received {0} packet", packet.GetType().Name);
break;
}
else if (exception is MqttCommunicationException)
{
_logger.Warning(exception, "Communication error while receiving packets");
}
else
{
_logger.Error(exception, "Error while processing received {0} packet", packet.GetType().Name);
}
// The packet dispatcher may already be gone due to disconnect etc.!
// The packet dispatcher may already be gone due to disconnect etc!
_packetDispatcher?.FailAll(exception);
await DisconnectInternal(_packetReceiverTask, exception, null).ConfigureAwait(false);
@@ -1096,13 +1029,10 @@ namespace MQTTnet.Client
if (timeWithoutPacketSent > keepAlivePeriod)
{
using (var pingTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
// We already reached the keep alive timeout. Due to the RFC part [MQTT-3.1.2-24] the server will wait another
// 1/2 of the keep alive time. So we can also use this value as the timeout.
pingTimeout.CancelAfter((int)(keepAlivePeriod.TotalMilliseconds / 2));
await PingAsync(pingTimeout.Token).ConfigureAwait(false);
timeoutCancellationTokenSource.CancelAfter(Options.Timeout);
await PingAsync(timeoutCancellationTokenSource.Token).ConfigureAwait(false);
}
}
@@ -1120,16 +1050,17 @@ namespace MQTTnet.Client
return;
}
switch (exception)
if (exception is OperationCanceledException)
{
case OperationCanceledException _:
return;
case MqttCommunicationException _:
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
break;
default:
_logger.Error(exception, "Error exception while sending/receiving keep alive packets");
break;
return;
}
else if (exception is MqttCommunicationException)
{
_logger.Warning(exception, "Communication error while sending/receiving keep alive packets");
}
else
{
_logger.Error(exception, "Error exception while sending/receiving keep alive packets");
}
await DisconnectInternal(_keepAlivePacketsSenderTask, exception, null).ConfigureAwait(false);
@@ -1140,4 +1071,4 @@ namespace MQTTnet.Client
}
}
}
}
}
@@ -1,36 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Security.Cryptography.X509Certificates;
namespace MQTTnet.Client
{
public sealed class MqttClientCertificateSelectionEventArgs : EventArgs
{
public MqttClientCertificateSelectionEventArgs(
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers,
MqttClientTcpOptions tcpOptions)
{
TargetHost = targetHost;
LocalCertificates = localCertificates;
RemoveCertificate = remoteCertificate;
AcceptableIssuers = acceptableIssuers;
TcpOptions = tcpOptions ?? throw new ArgumentNullException(nameof(tcpOptions));
}
public string[] AcceptableIssuers { get; }
public X509CertificateCollection LocalCertificates { get; }
public X509Certificate RemoveCertificate { get; }
public string TargetHost { get; }
public MqttClientTcpOptions TcpOptions { get; }
}
}
@@ -10,20 +10,11 @@ namespace MQTTnet.Client
{
public sealed class MqttClientCertificateValidationEventArgs : EventArgs
{
public MqttClientCertificateValidationEventArgs(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, IMqttClientChannelOptions clientOptions)
{
Certificate = certificate;
Chain = chain;
SslPolicyErrors = sslPolicyErrors;
ClientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
}
public X509Certificate Certificate { get; set; }
public X509Certificate Certificate { get; }
public X509Chain Chain { get; }
public IMqttClientChannelOptions ClientOptions { get; }
public X509Chain Chain { get; set; }
public IMqttClientChannelOptions ClientOptions { get; set; }
#if NET452 || NET461 || NET48
/// <summary>
/// Can be a host string name or an object derived from WebRequest.
@@ -31,6 +22,6 @@ namespace MQTTnet.Client
public object Sender { get; set; }
#endif
public SslPolicyErrors SslPolicyErrors { get; }
public SslPolicyErrors SslPolicyErrors { get; set; }
}
}
@@ -8,7 +8,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using MQTTnet.Formatter;
using MQTTnet.Packets;
@@ -79,26 +78,11 @@ namespace MQTTnet.Client
{
if (_port.HasValue)
{
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value, dns.AddressFamily);
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value);
}
else
{
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure, dns.AddressFamily);
}
}
}
if (_remoteEndPoint is IPEndPoint ip)
{
if (ip.Port == 0)
{
if (_port.HasValue)
{
_remoteEndPoint = new IPEndPoint(ip.Address, _port.Value);
}
else
{
_remoteEndPoint = new IPEndPoint(ip.Address, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
}
}
}
@@ -130,12 +114,6 @@ namespace MQTTnet.Client
return _options;
}
public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily)
{
_tcpOptions.AddressFamily = addressFamily;
return this;
}
public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data)
{
_options.AuthenticationMethod = method;
@@ -179,24 +157,33 @@ namespace MQTTnet.Client
{
case "tcp":
case "mqtt":
{
WithTcpServer(uri.Host, port);
break;
}
case "mqtts":
{
WithTcpServer(uri.Host, port)
.WithTlsOptions(
o =>
{
o.UseTls();
});
break;
}
case "ws":
case "wss":
{
WithWebSocketServer(o => o.WithUri(uri.ToString()));
break;
}
default:
throw new ArgumentException("Unexpected scheme in uri.");
{
throw new ArgumentException("Unexpected scheme in URI.");
}
}
if (!string.IsNullOrEmpty(uri.UserInfo))
@@ -282,7 +269,7 @@ namespace MQTTnet.Client
}
/// <summary>
/// The client will not throw an exception when the MQTT server responds with a non success ACK packet.
/// The client will not throw an exception when the MQTT server responses with a non success ACK packet.
/// This will become the default behavior in future versions of the library.
/// </summary>
public MqttClientOptionsBuilder WithoutThrowOnNonSuccessfulConnectResponse()
@@ -291,12 +278,6 @@ namespace MQTTnet.Client
return this;
}
public MqttClientOptionsBuilder WithProtocolType(ProtocolType protocolType)
{
_tcpOptions.ProtocolType = protocolType;
return this;
}
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
{
if (value == MqttProtocolVersion.Unknown)
@@ -367,18 +348,13 @@ namespace MQTTnet.Client
return this;
}
public MqttClientOptionsBuilder WithTcpServer(string host, int? port = null, AddressFamily addressFamily = AddressFamily.Unspecified)
public MqttClientOptionsBuilder WithTcpServer(string server, int? port = null)
{
if (host == null)
{
throw new ArgumentNullException(nameof(host));
}
_tcpOptions = new MqttClientTcpOptions();
// The value 0 will be updated when building the options.
// This a backward compatibility feature.
_remoteEndPoint = new DnsEndPoint(host, port ?? 0, addressFamily);
_remoteEndPoint = new DnsEndPoint(server, port ?? 0);
_port = port;
return this;
@@ -68,13 +68,8 @@ namespace MQTTnet.Client
Throw(nameof(options.AuthenticationMethod));
}
if (options.ExtendedAuthenticationExchangeHandler != null)
{
Throw(nameof(options.ExtendedAuthenticationExchangeHandler));
}
// Will relevant properties.
if (options.WillPayloadFormatIndicator != MqttPayloadFormatIndicator.Unspecified)
{
Throw(nameof(options.WillPayloadFormatIndicator));
@@ -2,16 +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;
using System.Net;
using System.Net.Sockets;
using MQTTnet.Protocol;
namespace MQTTnet.Client
{
public sealed class MqttClientTcpOptions : IMqttClientChannelOptions
{
EndPoint _remoteEndpoint;
public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified;
public int BufferSize { get; set; } = 8192;
@@ -23,12 +20,6 @@ namespace MQTTnet.Client
/// </summary>
public bool? DualMode { get; set; }
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
public int? Port { get; set; }
[Obsolete("Use RemoteEndpoint or MqttClientOptionsBuilder instead.")]
public string Server { get; set; }
public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
/// <summary>
@@ -37,70 +28,15 @@ namespace MQTTnet.Client
/// </summary>
public EndPoint LocalEndpoint { get; set; }
/// <summary>
/// Enables or disables the Nagle algorithm for the socket.
/// This is only supported for TCP.
/// For other protocol types the value is ignored.
/// Default: true
/// </summary>
public bool NoDelay { get; set; } = true;
/// <summary>
/// The MQTT transport is usually TCP but when using other endpoint types like
/// unix sockets it must be changed (IP for unix sockets).
/// </summary>
public ProtocolType ProtocolType { get; set; } = ProtocolType.Tcp;
public EndPoint RemoteEndpoint
{
get => _remoteEndpoint;
set
{
_remoteEndpoint = value;
if (_remoteEndpoint is DnsEndPoint dnsEndPoint)
{
Server = dnsEndPoint.Host;
Port = dnsEndPoint.Port;
}
else if (_remoteEndpoint is IPEndPoint ipEndPoint)
{
Server = ipEndPoint.Address.ToString();
Port = ipEndPoint.Port;
}
}
}
public EndPoint RemoteEndpoint { get; set; }
public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions();
public override string ToString()
{
if (RemoteEndpoint != null)
{
return RemoteEndpoint.ToString();
}
if (!string.IsNullOrEmpty(Server))
{
return $"{Server}:{GetPort()}";
}
return string.Empty;
}
int GetPort()
{
if (Port.HasValue)
{
return Port.Value;
}
if (TlsOptions?.UseTls == true)
{
return MqttPorts.Secure;
}
return MqttPorts.Default;
return RemoteEndpoint?.ToString() ?? string.Empty;
}
}
}
@@ -14,8 +14,6 @@ namespace MQTTnet.Client
{
public Func<MqttClientCertificateValidationEventArgs, bool> CertificateValidationHandler { get; set; }
public Func<MqttClientCertificateSelectionEventArgs, X509Certificate> CertificateSelectionHandler { get; set; }
public bool UseTls { get; set; }
public bool IgnoreCertificateRevocationErrors { get; set; }
@@ -49,17 +49,6 @@ namespace MQTTnet.Client
return this;
}
public MqttClientTlsOptionsBuilder WithCertificateSelectionHandler(Func<MqttClientCertificateSelectionEventArgs, X509Certificate> certificateSelectionHandler)
{
if (certificateSelectionHandler == null)
{
throw new ArgumentNullException(nameof(certificateSelectionHandler));
}
_tlsOptions.CertificateSelectionHandler = certificateSelectionHandler;
return this;
}
public MqttClientTlsOptionsBuilder WithClientCertificates(IEnumerable<X509Certificate2> certificates)
{
if (certificates == null)
@@ -1,19 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MQTTnet.Exceptions
{
public class MqttClientNotConnectedException : MqttCommunicationException
{
public MqttClientNotConnectedException() : base("The MQTT client is not connected.")
{
}
public MqttClientNotConnectedException(Exception innerException) : base("The MQTT client is not connected.", innerException)
{
}
}
}
@@ -68,12 +68,12 @@ namespace MQTTnet.Formatter.V5
MqttPacket DecodeAuthPacket(ArraySegment<byte> body)
{
ThrowIfBodyIsEmpty(body);
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
var packet = new MqttAuthPacket();
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
// In this case the AUTH has a Remaining Length of 0.
if (_bufferReader.EndOfStream)
{
packet.ReasonCode = MqttAuthenticateReasonCode.Success;
@@ -220,7 +220,7 @@ namespace MQTTnet.Formatter.V5
var packet = new MqttConnectPacket
{
// If the Request Problem Information is absent, the value of 1 is used.
RequestProblemInformation = true
RequestProblemInformation = true
};
var protocolName = _bufferReader.ReadString();
@@ -352,16 +352,7 @@ namespace MQTTnet.Formatter.V5
MqttPacket DecodeDisconnectPacket(ArraySegment<byte> body)
{
// From RFC: 3.14.2.1 Disconnect Reason Code
// Byte 1 in the Variable Header is the Disconnect Reason Code.
// If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used.
if (body.Count == 0)
{
return new MqttDisconnectPacket
{
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection
};
}
ThrowIfBodyIsEmpty(body);
_bufferReader.SetBuffer(body.Array, body.Offset, body.Count);
@@ -395,7 +386,7 @@ namespace MQTTnet.Formatter.V5
return packet;
}
MqttPacket DecodePubAckPacket(ArraySegment<byte> body)
{
ThrowIfBodyIsEmpty(body);
@@ -733,7 +724,7 @@ namespace MQTTnet.Formatter.V5
packet.UserProperties = propertiesReader.CollectedUserProperties;
packet.ReasonCodes = new List<MqttUnsubscribeReasonCode>(_bufferReader.BytesLeft);
while (!_bufferReader.EndOfStream)
{
var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte();
@@ -779,4 +770,4 @@ namespace MQTTnet.Formatter.V5
}
}
}
}
}
@@ -13,7 +13,7 @@ namespace MQTTnet.Formatter.V5
public sealed class MqttV5PacketEncoder
{
const int FixedHeaderSize = 1;
readonly MqttBufferWriter _bufferWriter;
readonly MqttV5PropertiesWriter _propertiesWriter = new MqttV5PropertiesWriter(new MqttBufferWriter(1024, 4096));
@@ -64,20 +64,13 @@ namespace MQTTnet.Formatter.V5
byte EncodeAuthPacket(MqttAuthPacket packet)
{
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod);
_propertiesWriter.WriteAuthenticationData(packet.AuthenticationData);
_propertiesWriter.WriteReasonString(packet.ReasonString);
_propertiesWriter.WriteUserProperties(packet.UserProperties);
// MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties.
// In this case the AUTH has a Remaining Length of 0.
if (packet.ReasonCode == MqttAuthenticateReasonCode.Success && _propertiesWriter.Length == 0)
{
return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Auth);
}
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter);
_propertiesWriter.Reset();
@@ -286,7 +279,7 @@ namespace MQTTnet.Formatter.V5
_propertiesWriter.WriteReasonString(packet.ReasonString);
_propertiesWriter.WriteUserProperties(packet.UserProperties);
if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubAckReasonCode.Success)
if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubAckReasonCode.Success)
{
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter);
@@ -382,7 +375,7 @@ namespace MQTTnet.Formatter.V5
_bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier);
if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubRecReasonCode.Success)
if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubRecReasonCode.Success)
{
_bufferWriter.WriteByte((byte)packet.ReasonCode);
_propertiesWriter.WriteTo(_bufferWriter);
@@ -533,7 +526,7 @@ namespace MQTTnet.Formatter.V5
static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttPacket packet)
{
// SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1].
// SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1].
if (packetIdentifier == 0)
{
@@ -541,4 +534,4 @@ namespace MQTTnet.Formatter.V5
}
}
}
}
}
@@ -22,20 +22,20 @@ namespace MQTTnet.Implementations
NetworkStream _networkStream;
public CrossPlatformSocket(AddressFamily addressFamily, ProtocolType protocolType)
public CrossPlatformSocket(AddressFamily addressFamily)
{
_socket = new Socket(addressFamily, SocketType.Stream, protocolType);
_socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
#if !NET5_0_OR_GREATER
_socketDisposeAction = _socket.Dispose;
#endif
}
public CrossPlatformSocket(ProtocolType protocolType)
public CrossPlatformSocket()
{
// Having this constructor is important because avoiding the address family as parameter
// will make use of dual mode in the .net framework.
_socket = new Socket(SocketType.Stream, protocolType);
_socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
#if !NET5_0_OR_GREATER
_socketDisposeAction = _socket.Dispose;
@@ -142,14 +142,12 @@ namespace MQTTnet.Implementations
set => _socket.SendTimeout = value;
}
public async Task<CrossPlatformSocket> AcceptAsync(CancellationToken cancellationToken)
public async Task<CrossPlatformSocket> AcceptAsync()
{
try
{
#if NET452 || NET461
var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false);
#elif NET7_0_OR_GREATER
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
#else
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
#endif
@@ -204,28 +202,9 @@ namespace MQTTnet.Implementations
using (cancellationToken.Register(_socketDisposeAction))
{
#if NET452 || NET461
// This is a fix for Mono which behaves differently than dotnet.
// The connection will not be established when the DNS endpoint is used.
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
{
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, dns.Host, dns.Port, null).ConfigureAwait(false);
}
else
{
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, endPoint, null).ConfigureAwait(false);
}
await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, endPoint, null).ConfigureAwait(false);
#else
// This is a fix for Mono which behaves differently than dotnet.
// The connection will not be established when the DNS endpoint is used.
if (endPoint is DnsEndPoint dns && dns.AddressFamily == AddressFamily.Unspecified)
{
await _socket.ConnectAsync(dns.Host, dns.Port).ConfigureAwait(false);
}
else
{
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
}
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
#endif
}
#endif
@@ -16,7 +16,6 @@ using MQTTnet.Channel;
using MQTTnet.Client;
using MQTTnet.Exceptions;
using MQTTnet.Internal;
using MQTTnet.Protocol;
namespace MQTTnet.Implementations
{
@@ -64,11 +63,11 @@ namespace MQTTnet.Implementations
{
if (_tcpOptions.AddressFamily == AddressFamily.Unspecified)
{
socket = new CrossPlatformSocket(_tcpOptions.ProtocolType);
socket = new CrossPlatformSocket();
}
else
{
socket = new CrossPlatformSocket(_tcpOptions.AddressFamily, _tcpOptions.ProtocolType);
socket = new CrossPlatformSocket(_tcpOptions.AddressFamily);
}
if (_tcpOptions.LocalEndpoint != null)
@@ -79,12 +78,7 @@ namespace MQTTnet.Implementations
socket.ReceiveBufferSize = _tcpOptions.BufferSize;
socket.SendBufferSize = _tcpOptions.BufferSize;
socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds;
if (_tcpOptions.ProtocolType == ProtocolType.Tcp)
{
// Other protocol types do not support the Nagle algorithm.
socket.NoDelay = _tcpOptions.NoDelay;
}
socket.NoDelay = _tcpOptions.NoDelay;
if (socket.LingerState != null)
{
@@ -99,29 +93,6 @@ namespace MQTTnet.Implementations
socket.DualMode = _tcpOptions.DualMode.Value;
}
// This block is only for backward compatibility.
if (_tcpOptions.RemoteEndpoint == null && !string.IsNullOrEmpty(_tcpOptions.Server))
{
int port;
if (_tcpOptions.Port.HasValue)
{
port = _tcpOptions.Port.Value;
}
else
{
if (_tcpOptions.TlsOptions?.UseTls == true)
{
port = MqttPorts.Secure;
}
else
{
port = MqttPorts.Default;
}
}
_tcpOptions.RemoteEndpoint = new DnsEndPoint(_tcpOptions.Server, port, AddressFamily.Unspecified);
}
await socket.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
@@ -135,29 +106,10 @@ namespace MQTTnet.Implementations
{
if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns)
{
targetHost = dns.Host;
targetHost = dns.Host;
}
}
SslStream sslStream;
if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null)
{
sslStream = new SslStream(
networkStream,
false,
InternalUserCertificateValidationCallback,
InternalUserCertificateSelectionCallback);
}
else
{
// Use a different constructor depending on the options for MQTTnet so that we do not have
// to copy the exact same behavior of the selection handler.
sslStream = new SslStream(
networkStream,
false,
InternalUserCertificateValidationCallback);
}
var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback);
try
{
#if NETCOREAPP3_1_OR_GREATER
@@ -182,7 +134,9 @@ namespace MQTTnet.Implementations
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown,
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode
RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors
? X509RevocationMode.NoCheck
: _tcpOptions.TlsOptions.RevocationMode
};
sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain);
@@ -338,34 +292,19 @@ namespace MQTTnet.Implementations
}
}
X509Certificate InternalUserCertificateSelectionCallback(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler;
if (certificateSelectionHandler != null)
{
var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions);
return certificateSelectionHandler(eventArgs);
}
if (localCertificates?.Count > 0)
{
return localCertificates[0];
}
return null;
}
bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null)
{
var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions);
var eventArgs = new MqttClientCertificateValidationEventArgs
{
Certificate = x509Certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _tcpOptions
};
return certificateValidationHandler(eventArgs);
}
@@ -27,7 +27,7 @@ namespace MQTTnet.Implementations
readonly MqttServerOptions _serverOptions;
readonly MqttServerTcpEndpointBaseOptions _options;
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
CrossPlatformSocket _socket;
IPEndPoint _localEndPoint;
@@ -65,7 +65,7 @@ namespace MQTTnet.Implementations
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
_socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp);
_socket = new CrossPlatformSocket(_addressFamily);
// Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2
if (_options.ReuseAddress)
@@ -87,33 +87,33 @@ namespace MQTTnet.Implementations
{
_socket.KeepAlive = _options.KeepAlive.Value;
}
if (_options.TcpKeepAliveInterval.HasValue)
{
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value;
}
if (_options.TcpKeepAliveRetryCount.HasValue)
{
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value;
}
if (_options.TcpKeepAliveTime.HasValue)
{
_socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value;
}
_socket.Bind(_localEndPoint);
// Get the local endpoint back from the socket. The port may have changed.
// This can happen when port 0 is used. Then the OS will choose the next free port.
_localEndPoint = (IPEndPoint)_socket.LocalEndPoint;
_options.Port = _localEndPoint.Port;
_socket.Listen(_options.ConnectionBacklog);
_logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint);
Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger);
return true;
@@ -141,7 +141,7 @@ namespace MQTTnet.Implementations
{
try
{
var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false);
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
if (clientSocket == null)
{
continue;
@@ -183,7 +183,7 @@ namespace MQTTnet.Implementations
clientSocket.NoDelay = _options.NoDelay;
stream = clientSocket.GetStream();
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
if (clientCertificate != null)
{
if (!clientCertificate.HasPrivateKey)
@@ -228,7 +228,7 @@ namespace MQTTnet.Implementations
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
{
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
@@ -109,7 +109,7 @@ namespace MQTTnet.Implementations
// A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2].
await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false);
#else
// The lock is required because the client will throw an exception if _SendAsync_ is
// The lock is required because the client will throw an exception if _SendAsync_ is
// called from multiple threads at the same time. But this issue only happens with several
// framework versions.
if (_sendLock == null)
@@ -156,7 +156,7 @@ namespace MQTTnet.Implementations
#else
var proxyUri = new Uri(_options.ProxyOptions.Address);
WebProxy webProxy;
if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password))
{
var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain);
@@ -164,9 +164,9 @@ namespace MQTTnet.Implementations
}
else
{
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList);
webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList);
}
if (_options.ProxyOptions.UseDefaultCredentials)
{
// Only update the property if required because setting it to false will alter
@@ -234,7 +234,7 @@ namespace MQTTnet.Implementations
{
clientWebSocket.Options.Credentials = _options.Credentials;
}
var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null)
{
@@ -245,11 +245,15 @@ namespace MQTTnet.Implementations
#elif WINDOWS_UWP
throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'.");
#elif NET452 || NET461 || NET48
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{
var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options)
var context = new MqttClientCertificateValidationEventArgs
{
Sender = sender
Sender = sender,
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
return certificateValidationHandler(context);
@@ -258,16 +262,17 @@ namespace MQTTnet.Implementations
clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
// TODO: Find a way to add client options to same callback. Problem is that they have a different type.
var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options);
var context = new MqttClientCertificateValidationEventArgs
{
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
return certificateValidationHandler(context);
};
#endif
var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler;
if (certificateSelectionHandler != null)
{
throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections.");
}
}
}
}
-5
View File
@@ -135,11 +135,6 @@ namespace MQTTnet
return new MqttClient(clientAdapterFactory, logger);
}
public MqttExtendedAuthenticationExchangeOptionsFactory CreateMqttExtendedAuthenticationExchangeOptionsBuilder()
{
return new MqttExtendedAuthenticationExchangeOptionsFactory();
}
public MqttServer CreateMqttServer(MqttServerOptions options)
{
return CreateMqttServer(options, DefaultLogger);
@@ -7,14 +7,11 @@ using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using MQTTnet.Adapter;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Exceptions;
namespace MQTTnet.Server
{
@@ -192,54 +189,5 @@ namespace MQTTnet.Server
/// A value of 0 indicates that the value is not used.
/// </summary>
public uint WillDelayInterval => _connectPacket.WillDelayInterval;
public async Task<MqttExtendedAuthenticationExchangeResult> SendExtendedAuthenticationExchangeDataAsync(
MqttExtendedAuthenticationExchangeOptions options,
CancellationToken cancellationToken = default)
{
if (options == null) throw new ArgumentNullException(nameof(options));
var requestAuthPacket = new MqttAuthPacket
{
// From RFC: If the initial CONNECT packet included an Authentication Method property then all AUTH packets,
// and any successful CONNACK packet MUST include an Authentication Method Property with the same value as in the CONNECT packet [MQTT-4.12.0-5].
AuthenticationMethod = AuthenticationMethod,
// The reason code will stay at continue all the time when connecting. The server will respond with the
// CONNACK packet when authentication is done!
ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication,
AuthenticationData = options.AuthenticationData,
ReasonString = options.ReasonString,
UserProperties = options.UserProperties
};
await ChannelAdapter.SendPacketAsync(requestAuthPacket, cancellationToken).ConfigureAwait(false);
var responsePacket = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false);
if (responsePacket == null)
{
throw new MqttCommunicationException("The client closed the connection.");
}
if (responsePacket is MqttAuthPacket responseAuthPacket)
{
if (!string.Equals(AuthenticationMethod, responseAuthPacket.AuthenticationMethod, StringComparison.Ordinal))
{
throw new MqttProtocolViolationException("The authentication method cannot change while authenticating the client.");
}
return MqttExtendedAuthenticationExchangeResultFactory.Create(responseAuthPacket);
}
if (responsePacket is MqttDisconnectPacket disconnectPacket)
{
return MqttExtendedAuthenticationExchangeResultFactory.Create(disconnectPacket);
}
throw new MqttProtocolViolationException("Received other packet than AUTH while authenticating.");
}
}
}
@@ -1,18 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System.Collections.Generic;
namespace MQTTnet.Server
{
public sealed class MqttExtendedAuthenticationExchangeOptions
{
public byte[] AuthenticationData { get; set; }
public string ReasonString { get; set; }
public List<MqttUserProperty> UserProperties { get; set; }
}
}
@@ -1,73 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text;
using System;
using MQTTnet.Packets;
namespace MQTTnet.Server
{
public sealed class MqttExtendedAuthenticationExchangeOptionsFactory
{
readonly MqttExtendedAuthenticationExchangeOptions _options = new MqttExtendedAuthenticationExchangeOptions();
public MqttExtendedAuthenticationExchangeOptions Build()
{
return _options;
}
public MqttExtendedAuthenticationExchangeOptionsFactory WithAuthenticationData(byte[] authenticationData)
{
_options.AuthenticationData = authenticationData;
return this;
}
public MqttExtendedAuthenticationExchangeOptionsFactory WithAuthenticationData(string authenticationData)
{
if (authenticationData == null)
{
_options.AuthenticationData = null;
}
else
{
_options.AuthenticationData = Encoding.UTF8.GetBytes(authenticationData);
}
return this;
}
public MqttExtendedAuthenticationExchangeOptionsFactory WithReasonString(string reasonString)
{
_options.ReasonString = reasonString;
return this;
}
public MqttExtendedAuthenticationExchangeOptionsFactory WithUserProperties(List<MqttUserProperty> userProperties)
{
_options.UserProperties = userProperties;
return this;
}
public MqttExtendedAuthenticationExchangeOptionsFactory WithUserProperty(string name, string value)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
}
@@ -1,18 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System.Collections.Generic;
namespace MQTTnet.Server
{
public sealed class MqttExtendedAuthenticationExchangeResult
{
public string ReasonString { get; set; }
public List<MqttUserProperty> UserProperties { get; set; }
public byte[] AuthenticationData { get; set; }
}
}
@@ -1,46 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System;
namespace MQTTnet.Server
{
public static class MqttExtendedAuthenticationExchangeResultFactory
{
public static MqttExtendedAuthenticationExchangeResult Create(MqttAuthPacket authPacket)
{
if (authPacket == null)
{
throw new ArgumentNullException(nameof(authPacket));
}
return new MqttExtendedAuthenticationExchangeResult
{
AuthenticationData = authPacket.AuthenticationData,
ReasonString = authPacket.ReasonString,
UserProperties = authPacket.UserProperties
};
}
public static MqttExtendedAuthenticationExchangeResult Create(MqttDisconnectPacket disconnectPacket)
{
if (disconnectPacket == null)
{
throw new ArgumentNullException(nameof(disconnectPacket));
}
return new MqttExtendedAuthenticationExchangeResult
{
AuthenticationData = null,
ReasonString = disconnectPacket.ReasonString,
UserProperties = disconnectPacket.UserProperties
// SessionExpiryInterval makes no sense because the connection is not yet made!
// ServerReferences makes no sense when the client initiated a DISCONNECT!
};
}
}
}
@@ -257,12 +257,7 @@ namespace MQTTnet.Server
if (matchingSubscribersCount == 0)
{
if (reasonCode == (int)MqttPubAckReasonCode.Success)
{
// Only change the value if it was success. Otherwise, we would hide an error or not authorized status.
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
}
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false);
}
}