diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ee70ac0..1941a25 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a report to help us improve. title: '' labels: '' assignees: '' @@ -11,11 +11,12 @@ assignees: '' A clear and concise description of what the bug is. ### Which project is your bug related to? -- [x] Client -- [ ] ManagedClient -- [ ] MQTTnet.Server standalone -- [ ] Server -- [ ] Generic + +- Client +- ManagedClient +- MQTTnet.Server standalone +- Server +- Generic ### To Reproduce Steps to reproduce the behavior: @@ -35,9 +36,11 @@ Add any other context about the problem here. Include debugging or logging information here: ```batch +\\ Put your logging output here. ``` ### Code example Please provide full code examples below where possible to make it easier for the developers to check your issues. ```csharp +\\ Put your code here. ``` diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index 8ec115e..fb7bdc3 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -1,6 +1,6 @@ --- name: Custom issue template -about: Describe this issue template's purpose here. +about: Do you have a question related to the project? Use this template. title: '' labels: '' assignees: '' @@ -10,9 +10,10 @@ assignees: '' ### Describe your question A clear and concise description of what you want to know. -### Which project is your bug related to? -- [x] Client -- [ ] ManagedClient -- [ ] MQTTnet.Server standalone -- [ ] Server -- [ ] Generic +### Which project is your question related to? + +- Client +- ManagedClient +- MQTTnet.Server standalone +- Server +- Generic diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 23a133d..c76e79f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: Feature request -about: Suggest an idea for this project +about: Suggest an idea for this project. title: '' labels: '' assignees: '' @@ -12,11 +12,12 @@ Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Example. I'm am trying to do [...] but [...] ### Which project is your feature request related to? -- [x] Client -- [ ] ManagedClient -- [ ] MQTTnet.Server standalone -- [ ] Server -- [ ] Generic + +- Client +- ManagedClient +- MQTTnet.Server standalone +- Server +- Generic ### Describe the solution you'd like A clear and concise description of what you want to happen. diff --git a/Build/MQTTnet.nuspec b/Build/MQTTnet.nuspec index 8fc2673..6aafdc6 100644 --- a/Build/MQTTnet.nuspec +++ b/Build/MQTTnet.nuspec @@ -19,6 +19,7 @@ * [Server] Fixed: Sending Large packets with AspnetCore based connection throws System.ArgumentException. * [Server] Fixed wrong usage of socket option _NoDelay_. * [Server] Added remote certificate validation callback (thanks to @rudacs). +* [Server] Add support for certificate passwords (thanks to @cslutgen). * [MQTTnet.Server] Added REST API for publishing basic messages. Copyright Christian Kratky 2016-2019 diff --git a/Source/MQTTnet.Server/Configuration/CertificateSettingsModel.cs b/Source/MQTTnet.Server/Configuration/CertificateSettingsModel.cs new file mode 100644 index 0000000..89eb48b --- /dev/null +++ b/Source/MQTTnet.Server/Configuration/CertificateSettingsModel.cs @@ -0,0 +1,35 @@ +using System.IO; + +namespace MQTTnet.Server.Configuration +{ + public class CertificateSettingsModel + { + /// + /// Path to certificate. + /// + public string Path { get; set; } + + /// + /// Password of certificate. + /// + public string Password { get; set; } + + /// + /// Read certificate file. + /// + public byte[] ReadCertificate() + { + if (string.IsNullOrEmpty(Path) || string.IsNullOrWhiteSpace(Path)) + { + throw new FileNotFoundException("No path set"); + } + + if (!File.Exists(Path)) + { + throw new FileNotFoundException($"Could not find Certificate in path: {Path}"); + } + + return File.ReadAllBytes(Path); + } + } +} diff --git a/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs b/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs index 8221390..8693268 100644 --- a/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs +++ b/Source/MQTTnet.Server/Configuration/TcpEndpointModel.cs @@ -9,9 +9,9 @@ namespace MQTTnet.Server.Configuration public class TcpEndPointModel { /// - /// Path to Certificate + /// Certificate settings. /// - public string CertificatePath { get; set; } + public CertificateSettingsModel Certificate { get; set; } /// /// Enabled / Disable @@ -33,25 +33,6 @@ namespace MQTTnet.Server.Configuration /// public int Port { get; set; } = 1883; - /// - /// Read Certificate file - /// - /// - public byte[] ReadCertificate() - { - if (string.IsNullOrEmpty(CertificatePath) || string.IsNullOrWhiteSpace(CertificatePath)) - { - throw new FileNotFoundException("No path set"); - } - - if (!File.Exists(CertificatePath)) - { - throw new FileNotFoundException($"Could not find Certificate in path: {CertificatePath}"); - } - - return File.ReadAllBytes(CertificatePath); - } - /// /// Read IPv4 /// diff --git a/Source/MQTTnet.Server/Mqtt/MqttServerService.cs b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs index efb0010..b8c463f 100644 --- a/Source/MQTTnet.Server/Mqtt/MqttServerService.cs +++ b/Source/MQTTnet.Server/Mqtt/MqttServerService.cs @@ -47,7 +47,7 @@ namespace MQTTnet.Server.Mqtt MqttSubscriptionInterceptor mqttSubscriptionInterceptor, MqttApplicationMessageInterceptor mqttApplicationMessageInterceptor, MqttServerStorage mqttServerStorage, - PythonScriptHostService pythonScriptHostService, + PythonScriptHostService pythonScriptHostService, ILogger logger) { _settings = mqttSettings ?? throw new ArgumentNullException(nameof(mqttSettings)); @@ -179,7 +179,7 @@ namespace MQTTnet.Server.Mqtt .WithApplicationMessageInterceptor(_mqttApplicationMessageInterceptor) .WithSubscriptionInterceptor(_mqttSubscriptionInterceptor) .WithStorage(_mqttServerStorage); - + // Configure unencrypted connections if (_settings.TcpEndPoint.Enabled) { @@ -210,9 +210,23 @@ namespace MQTTnet.Server.Mqtt { options .WithEncryptedEndpoint() - .WithEncryptionSslProtocol(SslProtocols.Tls12) - .WithEncryptionCertificate(_settings.EncryptedTcpEndPoint.ReadCertificate()); + .WithEncryptionSslProtocol(SslProtocols.Tls12); + + if (!string.IsNullOrEmpty(_settings.EncryptedTcpEndPoint?.Certificate?.Path)) + { + IMqttServerCertificateCredentials certificateCredentials = null; + if (!string.IsNullOrEmpty(_settings.EncryptedTcpEndPoint?.Certificate?.Password)) + { + certificateCredentials = new MqttServerCertificateCredentials + { + Password = _settings.EncryptedTcpEndPoint.Certificate.Password + }; + } + + options.WithEncryptionCertificate(_settings.EncryptedTcpEndPoint.Certificate.ReadCertificate(), certificateCredentials); + } + if (_settings.EncryptedTcpEndPoint.TryReadIPv4(out var address4)) { options.WithEncryptedEndpointBoundIPAddress(address4); diff --git a/Source/MQTTnet.Server/appsettings.json b/Source/MQTTnet.Server/appsettings.json index 8ea10d6..71eaf20 100644 --- a/Source/MQTTnet.Server/appsettings.json +++ b/Source/MQTTnet.Server/appsettings.json @@ -27,7 +27,10 @@ "IPv4": "*", "IPv6": "*", "Port": 8883, - "CertificatePath": "/absolute/path/to/pfx" + "Certificate": { + "Path": "/absolute/path/to/pfx", + "Password": "" + } }, "WebSocketEndPoint": { "Enabled": true, @@ -63,4 +66,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index a7aefd1..65a1ec9 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -139,6 +139,13 @@ namespace MQTTnet.Client.Options return this; } + public MqttClientOptionsBuilder WithCredentials(IMqttClientCredentials credentials) + { + _options.Credentials = credentials; + + return this; + } + public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) { _options.ExtendedAuthenticationExchangeHandler = handler; diff --git a/Source/MQTTnet/Formatter/MqttPacketReader.cs b/Source/MQTTnet/Formatter/MqttPacketReader.cs index 2589c5b..61698a1 100644 --- a/Source/MQTTnet/Formatter/MqttPacketReader.cs +++ b/Source/MQTTnet/Formatter/MqttPacketReader.cs @@ -21,8 +21,6 @@ namespace MQTTnet.Formatter { // The MQTT fixed header contains 1 byte of flags and at least 1 byte for the remaining data length. // So in all cases at least 2 bytes must be read for a complete MQTT packet. - // async/await is used here because the next packet is received in a couple of minutes so the performance - // impact is acceptable according to a useless waiting thread. var buffer = fixedHeaderBuffer; var totalBytesRead = 0; @@ -55,16 +53,7 @@ namespace MQTTnet.Formatter }; } -#if WINDOWS_UWP - // UWP will have a dead lock when calling this not async. var bodyLength = await ReadBodyLengthAsync(buffer[1], cancellationToken).ConfigureAwait(false); -#else - // Here the async/await pattern is not used because the overhead of context switches - // is too big for reading 1 byte in a row. We expect that the remaining data was sent - // directly after the initial bytes. If the client disconnects just in this moment we - // will get an exception anyway. - var bodyLength = ReadBodyLength(buffer[1], cancellationToken); -#endif if (!bodyLength.HasValue) { @@ -81,49 +70,6 @@ namespace MQTTnet.Formatter }; } -#if !WINDOWS_UWP - private int? ReadBodyLength(byte initialEncodedByte, CancellationToken cancellationToken) - { - var offset = 0; - var multiplier = 128; - var value = initialEncodedByte & 127; - int encodedByte = initialEncodedByte; - - while ((encodedByte & 128) != 0) - { - offset++; - if (offset > 3) - { - throw new MqttProtocolViolationException("Remaining length is invalid."); - } - - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - var readCount = _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).GetAwaiter().GetResult(); - - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (readCount == 0) - { - return null; - } - - encodedByte = _singleByteBuffer[0]; - - value += (encodedByte & 127) * multiplier; - multiplier *= 128; - } - - return value; - } -#else - private async Task ReadBodyLengthAsync(byte initialEncodedByte, CancellationToken cancellationToken) { var offset = 0; @@ -164,6 +110,5 @@ namespace MQTTnet.Formatter return value; } -#endif } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs index 0e28ad0..e3dcab8 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -48,7 +48,7 @@ namespace MQTTnet.Implementations throw new ArgumentException("TLS certificate is not set."); } - var tlsCertificate = new X509Certificate2(options.TlsEndpointOptions.Certificate); + var tlsCertificate = new X509Certificate2(options.TlsEndpointOptions.Certificate, options.TlsEndpointOptions.CertificateCredentials.Password); if (!tlsCertificate.HasPrivateKey) { throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 5e0251d..61ca517 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -41,13 +41,13 @@ RELEASE;NETSTANDARD1_3 - + - + diff --git a/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs new file mode 100644 index 0000000..3f5fe0e --- /dev/null +++ b/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs @@ -0,0 +1,4 @@ +public interface IMqttServerCertificateCredentials +{ + string Password { get; } +} diff --git a/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs new file mode 100644 index 0000000..05b6c5f --- /dev/null +++ b/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs @@ -0,0 +1,7 @@ +namespace MQTTnet.Server +{ + public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials + { + public string Password { get; set; } + } +} diff --git a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs index 1fcd981..c25af84 100644 --- a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs @@ -82,9 +82,10 @@ namespace MQTTnet.Server return this; } - public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value) + public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null) { _options.TlsEndpointOptions.Certificate = value; + _options.TlsEndpointOptions.CertificateCredentials = credentials; return this; } @@ -94,6 +95,16 @@ namespace MQTTnet.Server return this; } +#if !WINDOWS_UWP + public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false) + { + _options.TlsEndpointOptions.ClientCertificateRequired = true; + _options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation; + _options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback; + return this; + } +#endif + public MqttServerOptionsBuilder WithoutEncryptedEndpoint() { _options.TlsEndpointOptions.IsEnabled = false; @@ -107,7 +118,7 @@ namespace MQTTnet.Server return this; } #endif - + public MqttServerOptionsBuilder WithStorage(IMqttServerStorage value) { _options.Storage = value; diff --git a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs index 282bef9..9bf325b 100644 --- a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs +++ b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs @@ -12,6 +12,8 @@ namespace MQTTnet.Server public byte[] Certificate { get; set; } + public IMqttServerCertificateCredentials CertificateCredentials { get; set; } + public bool ClientCertificateRequired { get; set; } public bool CheckCertificateRevocation { get; set; } diff --git a/Tests/MQTTnet.Core.Tests/RPC_Tests.cs b/Tests/MQTTnet.Core.Tests/RPC_Tests.cs index b0babfa..9f03172 100644 --- a/Tests/MQTTnet.Core.Tests/RPC_Tests.cs +++ b/Tests/MQTTnet.Core.Tests/RPC_Tests.cs @@ -8,6 +8,8 @@ using MQTTnet.Client.Receiving; using MQTTnet.Exceptions; using MQTTnet.Extensions.Rpc; using MQTTnet.Protocol; +using MQTTnet.Client.Options; +using MQTTnet.Formatter; namespace MQTTnet.Tests { @@ -15,26 +17,39 @@ namespace MQTTnet.Tests public class RPC_Tests { [TestMethod] - public async Task Execute_Success() + public Task Execute_Success_With_QoS_0() { - using (var testEnvironment = new TestEnvironment()) - { - await testEnvironment.StartServerAsync(); - var responseSender = await testEnvironment.ConnectClientAsync(); - await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V311); + } - responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => - { - await responseSender.PublishAsync(e.ApplicationMessage.Topic + "/response", "pong"); - }); + [TestMethod] + public Task Execute_Success_With_QoS_1() + { + return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V311); + } - var requestSender = await testEnvironment.ConnectClientAsync(); + [TestMethod] + public Task Execute_Success_With_QoS_2() + { + return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V311); + } - var rpcClient = new MqttRpcClient(requestSender); - var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + [TestMethod] + public Task Execute_Success_With_QoS_0_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V500); + } - Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); - } + [TestMethod] + public Task Execute_Success_With_QoS_1_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V500); + } + + [TestMethod] + public Task Execute_Success_With_QoS_2_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V500); } [TestMethod] @@ -51,5 +66,27 @@ namespace MQTTnet.Tests await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); } } + + private async Task Execute_Success(MqttQualityOfServiceLevel qosLevel, MqttProtocolVersion protocolVersion) + { + using (var testEnvironment = new TestEnvironment()) + { + await testEnvironment.StartServerAsync(); + var responseSender = await testEnvironment.ConnectClientAsync(new MqttClientOptionsBuilder().WithProtocolVersion(protocolVersion)); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + + responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => + { + await responseSender.PublishAsync(e.ApplicationMessage.Topic + "/response", "pong"); + }); + + var requestSender = await testEnvironment.ConnectClientAsync(); + + var rpcClient = new MqttRpcClient(requestSender); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", qosLevel); + + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } + } } }