Skip to content

Commit 8beee38

Browse files
authored
Add TLS support (#27)
* Add TLS support * Use .NET members instead of Java-ish methods. * Add `UseSsl` to `ConnectionSettings` * * * Make `EnsureConnection` async, and use `CreateAsync` to open the actual connection. * Add a test to try and connect via TLS that fails. * * Add `ITlsSettings` interface * Add `TlsSettings` class * Pass TLS settings to Amqp ConnectionFactory * Ensure test passes
1 parent 17910e9 commit 8beee38

File tree

7 files changed

+288
-118
lines changed

7 files changed

+288
-118
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ nunit-agent*
1414
test-output.log
1515
TestResults.xml
1616
TestResult.xml
17-
Tests/coverage.xml
1817
test.sh
1918
*.VisualState.xml
2019
.vscode
@@ -121,7 +120,7 @@ projects/Unit*/TestResult.xml
121120
.*.sw?
122121

123122
# tests
124-
Tests/coverage.*
123+
coverage*
125124

126125
# docs
127126
docs/temp/

RabbitMQ.AMQP.Client/IConnection.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
namespace RabbitMQ.AMQP.Client;
44

5-
public class ConnectionException(string? message) : Exception(message);
5+
public class ConnectionException : Exception
6+
{
7+
public ConnectionException(string message) : base(message)
8+
{
9+
}
10+
11+
public ConnectionException(string message, Exception innerException) : base(message, innerException)
12+
{
13+
}
14+
}
615

716
public interface IConnection : ILifeCycle
817
{
@@ -12,6 +21,5 @@ public interface IConnection : ILifeCycle
1221

1322
IConsumerBuilder ConsumerBuilder();
1423

15-
1624
public ReadOnlyCollection<IPublisher> GetPublishers();
1725
}
Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,50 @@
1+
using System.Net.Security;
2+
using System.Security.Authentication;
3+
using System.Security.Cryptography.X509Certificates;
4+
15
namespace RabbitMQ.AMQP.Client;
26

3-
public interface IConnectionSettings
7+
public interface IConnectionSettings : IEquatable<IConnectionSettings>
48
{
5-
string Host();
6-
7-
int Port();
8-
9-
string VirtualHost();
10-
11-
12-
string User();
13-
14-
string Password();
15-
16-
string Scheme();
9+
string Host { get; }
10+
int Port { get; }
11+
string VirtualHost { get; }
12+
string User { get; }
13+
string Password { get; }
14+
string Scheme { get; }
15+
string ConnectionName { get; }
16+
string Path { get; }
17+
bool UseSsl { get; }
18+
ITlsSettings? TlsSettings { get; }
19+
}
1720

18-
string ConnectionName();
21+
/// <summary>
22+
/// Contains the TLS/SSL settings for a connection.
23+
/// </summary>
24+
public interface ITlsSettings
25+
{
26+
/// <summary>
27+
/// Client certificates to use for mutual authentication.
28+
/// </summary>
29+
X509CertificateCollection ClientCertificates { get; }
30+
31+
/// <summary>
32+
/// Supported protocols to use.
33+
/// </summary>
34+
SslProtocols Protocols { get; }
35+
36+
/// <summary>
37+
/// Specifies whether certificate revocation should be performed during handshake.
38+
/// </summary>
39+
bool CheckCertificateRevocation { get; }
40+
41+
/// <summary>
42+
/// Gets or sets a certificate validation callback to validate remote certificate.
43+
/// </summary>
44+
RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; }
45+
46+
/// <summary>
47+
/// Gets or sets a local certificate selection callback to select the certificate which should be used for authentication.
48+
/// </summary>
49+
LocalCertificateSelectionCallback? LocalCertificateSelectionCallback { get; }
1950
}

RabbitMQ.AMQP.Client/Impl/AbstractLifeCycle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ protected void OnNewStatus(State newState, Error? error)
3333
return;
3434
}
3535

36-
var oldStatus = State;
36+
State oldStatus = State;
3737
State = newState;
3838
ChangeState?.Invoke(this, oldStatus, newState, error);
3939
}

RabbitMQ.AMQP.Client/Impl/AmqpConnection.cs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public class AmqpConnection : AbstractLifeCycle, IConnection
3939
private const string ConnectionNotRecoveredMessage = "Connection not recovered";
4040
private readonly SemaphoreSlim _semaphoreClose = new(1, 1);
4141

42-
4342
// The native AMQP.Net Lite connection
4443
private Connection? _nativeConnection;
4544

@@ -71,7 +70,6 @@ private void ChangeConsumersStatus(State state, Error? error)
7170
}
7271
}
7372

74-
7573
private async Task ReconnectEntities()
7674
{
7775
await ReconnectPublishers().ConfigureAwait(false);
@@ -102,7 +100,6 @@ private async Task ReconnectConsumers()
102100
// TODO: Implement the semaphore to avoid multiple connections
103101
// private readonly SemaphoreSlim _semaphore = new(1, 1);
104102

105-
106103
/// <summary>
107104
/// Publishers contains all the publishers created by the connection.
108105
/// Each connection can have multiple publishers.
@@ -113,7 +110,6 @@ private async Task ReconnectConsumers()
113110

114111
internal ConcurrentDictionary<string, IConsumer> Consumers { get; } = new();
115112

116-
117113
public ReadOnlyCollection<IPublisher> GetPublishers()
118114
{
119115
return Publishers.Values.ToList().AsReadOnly();
@@ -179,14 +175,18 @@ public IConsumerBuilder ConsumerBuilder()
179175
return new AmqpConsumerBuilder(this);
180176
}
181177

182-
protected override Task OpenAsync()
178+
protected override async Task OpenAsync()
183179
{
184-
EnsureConnection();
185-
return base.OpenAsync();
180+
await EnsureConnection()
181+
.ConfigureAwait(false);
182+
await base.OpenAsync()
183+
.ConfigureAwait(false);
186184
}
187185

188-
private void EnsureConnection()
186+
private async Task EnsureConnection()
189187
{
188+
// TODO: do this!
189+
// await _semaphore.WaitAsync();
190190
try
191191
{
192192
if (_nativeConnection is { IsClosed: false })
@@ -196,22 +196,53 @@ private void EnsureConnection()
196196

197197
var open = new Open
198198
{
199-
HostName = $"vhost:{_connectionSettings.VirtualHost()}",
199+
HostName = $"vhost:{_connectionSettings.VirtualHost}",
200200
Properties = new Fields()
201201
{
202-
[new Symbol("connection_name")] = _connectionSettings.ConnectionName(),
202+
[new Symbol("connection_name")] = _connectionSettings.ConnectionName,
203203
}
204204
};
205205

206-
var manualReset = new ManualResetEvent(false);
207-
_nativeConnection = new Connection(_connectionSettings.Address, null, open, (connection, open1) =>
206+
void onOpened(Amqp.IConnection connection, Open open1)
208207
{
209-
manualReset.Set();
210208
Trace.WriteLine(TraceLevel.Verbose, $"Connection opened. Info: {ToString()}");
211209
OnNewStatus(State.Open, null);
212-
});
210+
}
211+
212+
var cf = new ConnectionFactory();
213+
214+
if (_connectionSettings.UseSsl && _connectionSettings.TlsSettings is not null)
215+
{
216+
cf.SSL.Protocols = _connectionSettings.TlsSettings.Protocols;
217+
cf.SSL.CheckCertificateRevocation = _connectionSettings.TlsSettings.CheckCertificateRevocation;
218+
219+
if (_connectionSettings.TlsSettings.ClientCertificates.Count > 0)
220+
{
221+
cf.SSL.ClientCertificates = _connectionSettings.TlsSettings.ClientCertificates;
222+
}
223+
224+
if (_connectionSettings.TlsSettings.LocalCertificateSelectionCallback is not null)
225+
{
226+
cf.SSL.LocalCertificateSelectionCallback = _connectionSettings.TlsSettings.LocalCertificateSelectionCallback;
227+
}
228+
229+
if (_connectionSettings.TlsSettings.RemoteCertificateValidationCallback is not null)
230+
{
231+
cf.SSL.RemoteCertificateValidationCallback = _connectionSettings.TlsSettings.RemoteCertificateValidationCallback;
232+
}
233+
}
234+
235+
try
236+
{
237+
_nativeConnection = await cf.CreateAsync(_connectionSettings.Address, open: open, onOpened: onOpened)
238+
.ConfigureAwait(false);
239+
}
240+
catch (Exception ex)
241+
{
242+
throw new ConnectionException(
243+
$"Connection failed. Info: {ToString()}", ex);
244+
}
213245

214-
manualReset.WaitOne(TimeSpan.FromSeconds(5));
215246
if (_nativeConnection.IsClosed)
216247
{
217248
throw new ConnectionException(
@@ -294,7 +325,8 @@ await Task.Run(async () =>
294325
await Task.Delay(TimeSpan.FromMilliseconds(next))
295326
.ConfigureAwait(false);
296327

297-
EnsureConnection();
328+
await EnsureConnection()
329+
.ConfigureAwait(false);
298330
connected = true;
299331
}
300332
catch (Exception e)

0 commit comments

Comments
 (0)