diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs index 61184c4d..23883365 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -61,27 +61,6 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter } } - public EndPoint RemoteEndPoint - { - get - { - // mqtt over tcp - if (_connection.RemoteEndPoint != null) - { - return _connection.RemoteEndPoint; - } - - // mqtt over websocket - var httpFeature = _connection.Features.Get(); - if (httpFeature?.RemoteIpAddress != null) - { - return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort); - } - - return null; - } - } - public bool IsSecureConnection { get @@ -104,8 +83,50 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter } } + public EndPoint LocalEndPoint + { + get + { + // mqtt over tcp + if (_connection.LocalEndPoint != null) + { + return _connection.LocalEndPoint; + } + + // mqtt over websocket + var httpFeature = _connection.Features.Get(); + if (httpFeature?.LocalIpAddress != null) + { + return new IPEndPoint(httpFeature.LocalIpAddress, httpFeature.LocalPort); + } + + return null; + } + } + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + public EndPoint RemoteEndPoint + { + get + { + // mqtt over tcp + if (_connection.RemoteEndPoint != null) + { + return _connection.RemoteEndPoint; + } + + // mqtt over websocket + var httpFeature = _connection.Features.Get(); + if (httpFeature?.RemoteIpAddress != null) + { + return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort); + } + + return null; + } + } + public async Task ConnectAsync(CancellationToken cancellationToken) { if (_connection is SocketConnection tcp && !tcp.IsConnected) @@ -232,8 +253,8 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter var span = output.GetSpan(buffer.Length); buffer.Packet.AsSpan().CopyTo(span); - int offset = buffer.Packet.Count; - buffer.Payload.CopyTo(destination: span.Slice(offset)); + var offset = buffer.Packet.Count; + buffer.Payload.CopyTo(span.Slice(offset)); output.Advance(buffer.Length); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs index 213cf19e..721f2c0b 100644 --- a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -79,6 +79,8 @@ public class SerializerBenchmark : BaseBenchmark public EndPoint RemoteEndPoint { get; set; } + public EndPoint LocalEndPoint { get; set; } + public bool IsSecureConnection { get; } = false; public X509Certificate2 ClientCertificate { get; set; } diff --git a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs index e539dc60..6d71f742 100644 --- a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs +++ b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs @@ -211,7 +211,7 @@ public sealed class MqttTcpServerListener : IDisposable var clientHandler = ClientHandler; if (clientHandler != null) { - var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); + var tcpChannel = new MqttTcpChannel(stream, _localEndPoint, remoteEndPoint, clientCertificate); var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index e49348bc..419c0493 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -14,6 +14,8 @@ true low latest-Recommended + default + disable diff --git a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs index 6d305686..1c9e3269 100644 --- a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs +++ b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs @@ -29,6 +29,8 @@ public sealed class MemoryMqttChannel : IMqttChannel public EndPoint RemoteEndPoint { get; set; } + public EndPoint LocalEndPoint { get; set; } + public bool IsSecureConnection { get; } = false; public X509Certificate2 ClientCertificate { get; set; } diff --git a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs index 8af3050c..bc65a0c7 100644 --- a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs @@ -45,7 +45,7 @@ public class MqttTcpChannel_Tests using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None); - var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), remoteEndPoint, null); + var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), new DnsEndPoint("localhost", 50000), remoteEndPoint, null); await Task.Delay(100, ct.Token); diff --git a/Source/MQTTnet.Tests/Server/General.cs b/Source/MQTTnet.Tests/Server/General.cs index d335b274..42905426 100644 --- a/Source/MQTTnet.Tests/Server/General.cs +++ b/Source/MQTTnet.Tests/Server/General.cs @@ -2,11 +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 Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Internal; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server; using System; using System.Buffers; using System.Collections.Generic; @@ -14,12 +9,18 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; namespace MQTTnet.Tests.Server; // ReSharper disable InconsistentNaming [TestClass] -public sealed class General_Tests : BaseTestClass +public class General_Tests : BaseTestClass { Dictionary _connected; @@ -83,23 +84,53 @@ public sealed class General_Tests : BaseTestClass } [TestMethod] - public async Task Deny_Connection() + public async Task Disconnect_Client_with_Reason() { using var testEnvironment = CreateTestEnvironment(); - testEnvironment.IgnoreClientLogErrors = true; + var disconnectPacketReceived = false; - var server = await testEnvironment.StartServer(); + string testClientId = null; - server.ValidatingConnectionAsync += e => + await testEnvironment.StartServer(); + + testEnvironment.Server.ClientConnectedAsync += e => { - e.ReasonCode = MqttConnectReasonCode.NotAuthorized; + testClientId = e.ClientId; return CompletedTask.Instance; }; var client = testEnvironment.CreateClient(); - var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions()); - Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode); + client.InspectPacketAsync += e => + { + if (e.Buffer.Length > 0) + { + if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4) + { + disconnectPacketReceived = true; + } + } + + return CompletedTask.Instance; + }; + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await LongTestDelay(); + + // Test client should be connected now + + Assert.IsNotNull(testClientId); + + // Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients + + await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.SessionTakenOver); + + await LongTestDelay(); + + // MQTT 3.1.1 client should not receive a disconnect packet + + Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected"); } [TestMethod] @@ -152,6 +183,27 @@ public sealed class General_Tests : BaseTestClass Assert.AreEqual("y", buffer.ToString()); } + [TestMethod] + public async Task Expose_Local_And_Remote_Endpoint() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + ValidatingConnectionEventArgs eventArgs = null; + server.ValidatingConnectionAsync += e => + { + eventArgs = e; + return Task.CompletedTask; + }; + + await testEnvironment.ConnectClient(); + + await Task.Delay(500); + + Assert.AreEqual("0.0.0.0:" + testEnvironment.ServerPort, eventArgs.ChannelAdapter.LocalEndPoint.ToString()); + Assert.StartsWith("127.0.0.1:", eventArgs.ChannelAdapter.RemoteEndPoint.ToString()); + } + [TestMethod] public async Task Handle_Clean_Disconnect() { @@ -205,35 +257,34 @@ public sealed class General_Tests : BaseTestClass var testEnvironment2 = testEnvironment; tasks.Add( - Task.Run( - async () => + Task.Run(async () => + { + try { - try - { - using var client = await testEnvironment2.ConnectClient(); - // Clear retained message. - await client.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload(EmptyBuffer.Array) - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); + using var client = await testEnvironment2.ConnectClient(); + // Clear retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload(EmptyBuffer.Array) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); - // Set retained message. - await client.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload("value") - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); + // Set retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload("value") + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); - await client.DisconnectAsync(); - } - catch (Exception exception) - { - testEnvironment2.TrackException(exception); - } - })); + await client.DisconnectAsync(); + } + catch (Exception exception) + { + testEnvironment2.TrackException(exception); + } + })); await Task.Delay(10); } @@ -432,21 +483,21 @@ public sealed class General_Tests : BaseTestClass } [TestMethod] - public async Task Publish_At_Least_Once_0x01() + public Task Publish_At_Least_Once_0x01() { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1); + return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1); } [TestMethod] - public async Task Publish_At_Most_Once_0x00() + public Task Publish_At_Most_Once_0x00() { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1); + return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1); } [TestMethod] - public async Task Publish_Exactly_Once_0x02() + public Task Publish_Exactly_Once_0x02() { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1); + return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1); } [TestMethod] @@ -886,59 +937,6 @@ public sealed class General_Tests : BaseTestClass Assert.IsTrue(client.IsConnected); } - [TestMethod] - public async Task Disconnect_Client_with_Reason() - { - using var testEnvironment = CreateTestEnvironment(); - var disconnectPacketReceived = false; - - string testClientId = null; - - await testEnvironment.StartServer(); - - testEnvironment.Server.ClientConnectedAsync += e => - { - testClientId = e.ClientId; - return CompletedTask.Instance; - }; - - var client = testEnvironment.CreateClient(); - - client.InspectPacketAsync += e => - { - if (e.Buffer.Length > 0) - { - if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4) - { - disconnectPacketReceived = true; - } - } - return CompletedTask.Instance; - }; - - await client.ConnectAsync( - new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort) - .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V311) - .Build() - ); - - await LongTestDelay(); - - // Test client should be connected now - - Assert.IsNotNull(testClientId); - - // Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients - - await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.SessionTakenOver); - - await LongTestDelay(); - - // MQTT 3.1.1 client should not receive a disconnect packet - - Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected"); - } - void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs) { if (!_connected.TryAdd(eventArgs.ClientId, true)) diff --git a/Source/MQTTnet.Tests/Server/Security_Tests.cs b/Source/MQTTnet.Tests/Server/Security_Tests.cs index 9b91eead..e27aff86 100644 --- a/Source/MQTTnet.Tests/Server/Security_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Security_Tests.cs @@ -14,8 +14,28 @@ namespace MQTTnet.Tests.Server; // ReSharper disable InconsistentNaming [TestClass] -public sealed class Security_Tests : BaseTestClass +public class Security_Tests : BaseTestClass { + [TestMethod] + public async Task Deny_Connection() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + e.ReasonCode = MqttConnectReasonCode.NotAuthorized; + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions()); + + Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode); + } + [TestMethod] public async Task Do_Not_Affect_Authorized_Clients() { diff --git a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs index 9337354a..2c1548a2 100644 --- a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs @@ -22,6 +22,8 @@ public interface IMqttChannelAdapter : IDisposable EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool IsSecureConnection { get; } MqttPacketFormatterAdapter PacketFormatterAdapter { get; } diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 724d47df..c1098a52 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -54,6 +54,8 @@ public sealed class MqttChannelAdapter : Disposable, IMqttChannelAdapter public EndPoint RemoteEndPoint => _channel.RemoteEndPoint; + public EndPoint LocalEndPoint => _channel.LocalEndPoint; + public bool IsSecureConnection => _channel.IsSecureConnection; public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } diff --git a/Source/MQTTnet/Channel/IMqttChannel.cs b/Source/MQTTnet/Channel/IMqttChannel.cs index 02af43e4..1eb2b3b9 100644 --- a/Source/MQTTnet/Channel/IMqttChannel.cs +++ b/Source/MQTTnet/Channel/IMqttChannel.cs @@ -14,8 +14,11 @@ namespace MQTTnet.Channel; public interface IMqttChannel : IDisposable { X509Certificate2 ClientCertificate { get; } + EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool IsSecureConnection { get; } Task ConnectAsync(CancellationToken cancellationToken); diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index 7333261c..96d5a26f 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -37,10 +37,11 @@ public sealed class MqttTcpChannel : IMqttChannel IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; } - public MqttTcpChannel(Stream stream, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this() + public MqttTcpChannel(Stream stream, EndPoint localEndPoint, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this() { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; IsSecureConnection = stream is SslStream; @@ -49,10 +50,13 @@ public sealed class MqttTcpChannel : IMqttChannel public X509Certificate2 ClientCertificate { get; } - public EndPoint RemoteEndPoint { get; private set; } - public bool IsSecureConnection { get; } + + public EndPoint LocalEndPoint { get; private set; } + + public EndPoint RemoteEndPoint { get; private set; } + public async Task ConnectAsync(CancellationToken cancellationToken) { CrossPlatformSocket socket = null; @@ -87,7 +91,7 @@ public sealed class MqttTcpChannel : IMqttChannel socket.LingerState = _tcpOptions.LingerState; } - if (_tcpOptions.DualMode.HasValue) + if (_tcpOptions.DualMode != null) { // It is important to avoid setting the flag if no specific value is set by the user // because on IPv4 only networks the setter will always throw an exception. Regardless @@ -115,37 +119,20 @@ public sealed class MqttTcpChannel : IMqttChannel SslStream sslStream; if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null) { - sslStream = new SslStream( - networkStream, - false, - InternalUserCertificateValidationCallback, - InternalUserCertificateSelectionCallback); + 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); + sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback); } try { - var sslOptions = new SslClientAuthenticationOptions - { - ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, - ClientCertificates = LoadCertificates(), - EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, - CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors - ? X509RevocationMode.NoCheck - : _tcpOptions.TlsOptions.RevocationMode, - TargetHost = targetHost, - CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy, - EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy, - AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation - }; + var sslOptions = CreateSslAuthenticationOptions(); + + sslOptions.TargetHost = targetHost; if (_tcpOptions.TlsOptions.TrustChain?.Count > 0) { @@ -176,6 +163,7 @@ public sealed class MqttTcpChannel : IMqttChannel } RemoteEndPoint = socket.RemoteEndPoint; + LocalEndPoint = socket.LocalEndPoint; } catch { @@ -281,6 +269,21 @@ public sealed class MqttTcpChannel : IMqttChannel } } + SslClientAuthenticationOptions CreateSslAuthenticationOptions() + { + var sslOptions = new SslClientAuthenticationOptions + { + ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, + ClientCertificates = LoadCertificates(), + EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, + CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode, + CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy, + EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy, + AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation + }; + return sslOptions; + } + X509Certificate InternalUserCertificateSelectionCallback( object sender, string targetHost, diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index 701cc279..3f50b26d 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -39,6 +39,8 @@ public sealed class MqttWebSocketChannel : IMqttChannel public EndPoint RemoteEndPoint { get; } + public EndPoint LocalEndPoint { get; } + public bool IsSecureConnection { get; private set; } public async Task ConnectAsync(CancellationToken cancellationToken) diff --git a/Source/ReleaseNotes.md b/Source/ReleaseNotes.md index e7cd57d4..d18fa9dc 100644 --- a/Source/ReleaseNotes.md +++ b/Source/ReleaseNotes.md @@ -2,6 +2,7 @@ * Core: Performance improvements * Server: Improved performance of retained messages when no event handler is attached (#2093, thanks to @zhaowgit) * Server: The event `InterceptingClientEnqueue` is now also called for retained messages (BREAKING CHANGE!) +* Server: The local end point is now also exposed in the channel adapter (#2179) * Marked all projects as AOT compatible * Restored the strong name of the nugets * Embedded license file in all nugets (#2197, thanks to @JensNordenbro)