Add support for certificate validation callback for Web Socket connections.

This commit is contained in:
Christian Kratky
2020-04-18 13:46:20 +02:00
parent bb5f023b74
commit 7931af7487
15 changed files with 234 additions and 113 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ namespace MQTTnet.Client
{
get
{
return _isConnected || Interlocked.Read(ref _isDisconnectPending) != 0;
return _isConnected && Interlocked.Read(ref _isDisconnectPending) == 0;
}
}
@@ -0,0 +1,16 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace MQTTnet.Client.Options
{
public class MqttClientCertificateValidationCallbackContext
{
public X509Certificate Certificate { get; set; }
public X509Chain Chain { get; set; }
public SslPolicyErrors SslPolicyErrors { get; set; }
public IMqttClientChannelOptions ClientOptions { get; set; }
}
}
@@ -262,6 +262,7 @@ namespace MQTTnet.Client.Options
Certificates = _tlsParameters.Certificates?.ToList(),
#endif
CertificateValidationCallback = _tlsParameters.CertificateValidationCallback,
CertificateValidationHandler = _tlsParameters.CertificateValidationHandler,
IgnoreCertificateChainErrors = _tlsParameters.IgnoreCertificateChainErrors,
IgnoreCertificateRevocationErrors = _tlsParameters.IgnoreCertificateRevocationErrors
};
@@ -10,12 +10,15 @@ namespace MQTTnet.Client.Options
{
public bool UseTls { get; set; }
[Obsolete("This property will be removed soon. Use CertificateValidationHandler instead.")]
public Func<X509Certificate, X509Chain, SslPolicyErrors, IMqttClientOptions, bool> CertificateValidationCallback
{
get;
set;
}
public Func<MqttClientCertificateValidationCallbackContext, bool> CertificateValidationHandler { get; set; }
public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12;
#if WINDOWS_UWP
@@ -23,7 +26,6 @@ namespace MQTTnet.Client.Options
#else
public IEnumerable<X509Certificate> Certificates { get; set; }
#endif
public bool AllowUntrustedCertificates { get; set; }
@@ -15,6 +15,7 @@ namespace MQTTnet.Client.Options
public bool IgnoreCertificateChainErrors { get; set; }
public bool AllowUntrustedCertificates { get; set; }
#if WINDOWS_UWP
public List<byte[]> Certificates { get; set; }
#else
@@ -23,6 +24,9 @@ namespace MQTTnet.Client.Options
public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12;
[Obsolete("This property will be removed soon. Use CertificateValidationHandler instead.")]
public Func<X509Certificate, X509Chain, SslPolicyErrors, IMqttClientOptions, bool> CertificateValidationCallback { get; set; }
public Func<MqttClientCertificateValidationCallbackContext, bool> CertificateValidationHandler { get; set; }
}
}
@@ -16,6 +16,8 @@
return "netstandard1.3";
#elif NETSTANDARD2_0
return "netstandard2.0";
#elif NETSTANDARD2_1
return "netstandard2.1";
#elif WINDOWS_UWP
return "uap10.0";
#endif
@@ -73,7 +73,7 @@ namespace MQTTnet.Implementations
var networkStream = socket.GetStream();
if (_options.TlsOptions.UseTls)
if (_options.TlsOptions?.UseTls == true)
{
var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback);
try
@@ -181,9 +181,28 @@ namespace MQTTnet.Implementations
bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (_options.TlsOptions.CertificateValidationCallback != null)
#region OBSOLETE
var certificateValidationCallback = _options?.TlsOptions?.CertificateValidationCallback;
if (certificateValidationCallback != null)
{
return _options.TlsOptions.CertificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _clientOptions);
return certificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _clientOptions);
}
#endregion
var certificateValidationHandler = _options?.TlsOptions?.CertificateValidationHandler;
if (certificateValidationHandler != null)
{
var context = new MqttClientCertificateValidationCallbackContext
{
Certificate = x509Certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
return certificateValidationHandler(context);
}
if (sslPolicyErrors == SslPolicyErrors.None)
@@ -1,21 +1,21 @@
using System;
using MQTTnet.Channel;
using MQTTnet.Client.Options;
using MQTTnet.Internal;
using System;
using System.Net;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Channel;
using MQTTnet.Client.Options;
using MQTTnet.Internal;
namespace MQTTnet.Implementations
{
public class MqttWebSocketChannel : Disposable, IMqttChannel
public sealed class MqttWebSocketChannel : IMqttChannel
{
private readonly MqttClientWebSocketOptions _options;
readonly MqttClientWebSocketOptions _options;
private SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
private WebSocket _webSocket;
AsyncLock _sendLock = new AsyncLock();
WebSocket _webSocket;
public MqttWebSocketChannel(MqttClientWebSocketOptions options)
{
@@ -53,6 +53,67 @@ namespace MQTTnet.Implementations
}
var clientWebSocket = new ClientWebSocket();
try
{
SetupClientWebSocket(clientWebSocket);
await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false);
}
catch (Exception)
{
// Prevent a memory leak when always creating new instance which will fail while connecting.
clientWebSocket.Dispose();
throw;
}
_webSocket = clientWebSocket;
IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase);
}
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
if (_webSocket == null)
{
return;
}
if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting)
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false);
}
Cleanup();
}
public async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var response = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken).ConfigureAwait(false);
return response.Count;
}
public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
// 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)
{
return;
}
using (await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false))
{
await _webSocket.SendAsync(new ArraySegment<byte>(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false);
}
}
public void Dispose()
{
Cleanup();
}
void SetupClientWebSocket(ClientWebSocket clientWebSocket)
{
if (_options.ProxyOptions != null)
{
@@ -90,68 +151,37 @@ namespace MQTTnet.Implementations
#else
clientWebSocket.Options.ClientCertificates.Add(certificate);
#endif
}
}
await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false);
_webSocket = clientWebSocket;
var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler;
#if NETSTANDARD2_1
if (certificateValidationHandler != null)
{
clientWebSocket.Options.RemoteCertificateValidationCallback = new System.Net.Security.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 MqttClientCertificateValidationCallbackContext
{
Certificate = certificate,
Chain = chain,
SslPolicyErrors = sslPolicyErrors,
ClientOptions = _options
};
IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase);
return certificateValidationHandler(context);
});
}
#else
if (certificateValidationHandler != null)
{
throw new NotSupportedException("The remote certificate validation callback for Web Sockets is only supported for netstandard 2.1+");
}
#endif
}
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
if (_webSocket == null)
{
return;
}
if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting)
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false);
}
Cleanup();
}
public async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var response = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken).ConfigureAwait(false);
return response.Count;
}
public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
// 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)
{
return;
}
await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await _webSocket.SendAsync(new ArraySegment<byte>(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false);
}
finally
{
_sendLock?.Release();
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Cleanup();
}
base.Dispose(disposing);
}
private void Cleanup()
void Cleanup()
{
_sendLock?.Dispose();
_sendLock = null;
@@ -169,7 +199,7 @@ namespace MQTTnet.Implementations
}
}
private IWebProxy CreateProxy()
IWebProxy CreateProxy()
{
if (string.IsNullOrEmpty(_options.ProxyOptions?.Address))
{
+22 -20
View File
@@ -4,11 +4,12 @@ using System.Threading;
namespace MQTTnet.Internal
{
public class BlockingQueue<TItem> : Disposable
public sealed class BlockingQueue<TItem> : IDisposable
{
private readonly object _syncRoot = new object();
private readonly LinkedList<TItem> _items = new LinkedList<TItem>();
private readonly ManualResetEventSlim _gate = new ManualResetEventSlim(false);
readonly object _syncRoot = new object();
readonly LinkedList<TItem> _items = new LinkedList<TItem>();
ManualResetEventSlim _gate = new ManualResetEventSlim(false);
public int Count
{
@@ -28,13 +29,13 @@ namespace MQTTnet.Internal
lock (_syncRoot)
{
_items.AddLast(item);
_gate.Set();
_gate?.Set();
}
}
public TItem Dequeue(CancellationToken cancellationToken = default(CancellationToken))
public TItem Dequeue(CancellationToken cancellationToken = default)
{
while (true)
while (!cancellationToken.IsCancellationRequested)
{
lock (_syncRoot)
{
@@ -48,17 +49,19 @@ namespace MQTTnet.Internal
if (_items.Count == 0)
{
_gate.Reset();
_gate?.Reset();
}
}
_gate.Wait(cancellationToken);
_gate?.Wait(cancellationToken);
}
throw new OperationCanceledException();
}
public TItem PeekAndWait(CancellationToken cancellationToken = default(CancellationToken))
public TItem PeekAndWait(CancellationToken cancellationToken = default)
{
while (true)
while (!cancellationToken.IsCancellationRequested)
{
lock (_syncRoot)
{
@@ -69,12 +72,14 @@ namespace MQTTnet.Internal
if (_items.Count == 0)
{
_gate.Reset();
_gate?.Reset();
}
}
_gate.Wait(cancellationToken);
_gate?.Wait(cancellationToken);
}
throw new OperationCanceledException();
}
public void RemoveFirst(Predicate<TItem> match)
@@ -109,13 +114,10 @@ namespace MQTTnet.Internal
}
}
protected override void Dispose(bool disposing)
public void Dispose()
{
if (disposing)
{
_gate.Dispose();
}
base.Dispose(disposing);
_gate?.Dispose();
_gate = null;
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net452;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' AND '$(MSBuildRuntimeType)' != 'Core' AND '$(SolutionName)' != 'MQTTnet.noUWP' ">$(TargetFrameworks);uap10.0</TargetFrameworks>
<AssemblyName>MQTTnet</AssemblyName>