Allow user properties as pre-encoded UTF-8 binary buffer (#2228)

* Add support for binary user properties in MQTT 5.0

* Optimize byte array reversal in SubscriptionTopicHashTests

* Use InvariantCulture for topic string conversion in Load_Tests

* switched to `Microsoft.Testing.Platform`

* Refactor CI workflow to separate signed and unsigned package builds

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

* Remove fallback to value as string in MqttUserProperty

* Refactor MqttUserProperty handling to use byte arrays for improved performance and add deprecation warnings for legacy methods
This commit is contained in:
Alexander Köpke
2025-12-18 17:46:35 +01:00
committed by GitHub
parent 1a3e97afec
commit 07409e3a35
31 changed files with 494 additions and 73 deletions
@@ -43,7 +43,7 @@ public sealed class Cross_Version_Tests : BaseTestClass
var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message")
.WithPayload("My_Payload")
.WithUserProperty("A", "B")
.WithUserProperty("A", Encoding.UTF8.GetBytes("B"))
.WithResponseTopic("Response")
.WithCorrelationData(Encoding.UTF8.GetBytes("Correlation"))
.Build();
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -26,6 +27,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
@@ -47,6 +49,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
@@ -70,6 +73,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
await client01.Connect();
@@ -91,6 +95,7 @@ public sealed class HotSwapCerts_Tests
using var client01 = new ClientTestHarness();
server.InstallNewClientCert(client01.GetCurrentClientCert());
client01.InstallNewServerCert(server.GetCurrentServerCert());
client01.SetServerPort(server.EncryptedPort);
await server.StartServer();
await client01.Connect();
@@ -135,10 +140,11 @@ public sealed class HotSwapCerts_Tests
sealed class ClientTestHarness : IDisposable
{
readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider();
int _serverPort = 8883;
IMqttClient _client;
public string ClientId => _client.Options.ClientId;
public string ClientId => _client?.Options?.ClientId;
public Task Connect()
{
@@ -147,10 +153,15 @@ public sealed class HotSwapCerts_Tests
public void Dispose()
{
_client.Dispose();
_client?.Dispose();
_hotSwapClient.Dispose();
}
public void SetServerPort(int port)
{
_serverPort = port;
}
public X509Certificate2 GetCurrentClientCert()
{
var result = _hotSwapClient.GetCertificates()[0];
@@ -216,7 +227,7 @@ public sealed class HotSwapCerts_Tests
o => o.WithClientCertificatesProvider(_hotSwapClient)
.WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation)
.WithSslProtocols(SslProtocols.Tls12))
.WithTcpServer("localhost")
.WithTcpServer("localhost", _serverPort)
.WithCleanSession()
.WithProtocolVersion(MqttProtocolVersion.V500);
@@ -242,6 +253,7 @@ public sealed class HotSwapCerts_Tests
sealed class ServerTestHarness : IDisposable
{
readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider();
readonly int _encryptedPort = GetFreeTcpPort();
MqttServer _server;
@@ -261,6 +273,8 @@ public sealed class HotSwapCerts_Tests
return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError);
}
public int EncryptedPort => _encryptedPort;
public X509Certificate2 GetCurrentServerCert()
{
return _hotSwapServer.GetCertificate();
@@ -281,6 +295,7 @@ public sealed class HotSwapCerts_Tests
var mqttServerFactory = new MqttServerFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer)
.WithEncryptedEndpointPort(_encryptedPort)
.WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback)
.WithEncryptedEndpoint()
.Build();
@@ -290,6 +305,17 @@ public sealed class HotSwapCerts_Tests
_server = mqttServerFactory.CreateMqttServer(mqttServerOptions);
return _server.StartAsync();
}
static int GetFreeTcpPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
sealed class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -98,7 +99,7 @@ public sealed class Subscribe_Tests : BaseTestClass
var client = await testEnvironment.ConnectClient();
var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build();
await client.SubscribeAsync(subscribeOptions);
CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Linq;
using System.Text;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
@@ -45,7 +46,7 @@ public sealed class Unsubscribe_Tests : BaseTestClass
var client = await testEnvironment.ConnectClient();
var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build();
var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build();
await client.UnsubscribeAsync(unsubscribeOptions);
CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties);
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
@@ -26,10 +27,10 @@ public class Feature_Tests
var message = new MqttApplicationMessageBuilder()
.WithTopic("A")
.WithUserProperty("x", "1")
.WithUserProperty("y", "2")
.WithUserProperty("z", "3")
.WithUserProperty("z", "4"); // z is here two times to test list of items
.WithUserProperty("x", Encoding.UTF8.GetBytes("1"))
.WithUserProperty("y", Encoding.UTF8.GetBytes("2"))
.WithUserProperty("z", Encoding.UTF8.GetBytes("3"))
.WithUserProperty("z", Encoding.UTF8.GetBytes("4")); // z is here two times to test list of items
await receiver.SubscribeAsync(new MqttClientSubscribeOptions
{