Skip to content

Commit 414fce5

Browse files
authored
Dead letter message when CLR is offline (#658)
- If certificate cannot be validated because of network related issues with CLR then it will threat it as an unhandeled exception instead of place it on the senders error queue. It will retry the message and if the problem presist then it will eventually be dead lettered. Dead lettered messages on receivers queue can be moved back to normal queue and retried when the issue have been resolved - Added support to dead letter messages - Added X509Chain Mock to tests and fixed failing and ignored tests on build server. Removed ignore and X509Chain test category because these tests runs on build server now. - Removed X509Chain test category after fixing the test to run on build server using X509Chain Mock - Remove calles of obsolete methods in MessagingException that were not in use either - dist: Bump version number to 5.1.0
1 parent 6b003c6 commit 414fce5

21 files changed

+336
-83
lines changed

Directory.build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<LangVersion>10.0</LangVersion>
4-
<Version>5.0.10</Version>
4+
<Version>5.1.0</Version>
55
</PropertyGroup>
66
<PropertyGroup>
77
<Authors>Norsk Helsenett SF</Authors>

src/Helsenorge.Messaging/Abstractions/MessagingException.cs

+8-17
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ public static class EventIds
139139
/// Invalid HER-id.
140140
/// </summary>
141141
public static EventId CouldNotVerifyCertificate = new EventId(41, EventIdName);
142+
/// <summary>
143+
/// Remote certificate has been revoked.
144+
/// </summary>
145+
public static EventId RemoteCertificateRevocationOffline = new EventId(42, EventIdName);
146+
/// <summary>
147+
/// Local certificate has been revoked.
148+
/// </summary>
149+
public static EventId LocalCertificateRevocationOffline = new EventId(43, EventIdName);
142150

143151
/// <summary>
144152
/// Event Id used for informational purposes when starting/ending the Receive process.
@@ -207,22 +215,5 @@ public MessagingException(string message) : base(message){}
207215
/// Constructor
208216
/// </summary>
209217
public MessagingException(string message, Exception inner) : base(message, inner){}
210-
/// <summary>
211-
/// Constructor
212-
/// </summary>
213-
protected MessagingException(
214-
SerializationInfo info,
215-
StreamingContext context) : base(info, context){}
216-
/// <summary>
217-
///
218-
/// </summary>
219-
/// <param name="info"></param>
220-
/// <param name="context"></param>
221-
// ReSharper disable once RedundantOverridenMember
222-
public override void GetObjectData(SerializationInfo info, StreamingContext context)
223-
{
224-
// code analysis trigger if this is not present
225-
base.GetObjectData(info, context);
226-
}
227218
}
228219
}

src/Helsenorge.Messaging/Amqp/AmqpCore.cs

+21
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,27 @@ internal static void RemoveProcessedMessageFromQueue(IAmqpMessage message)
469469

470470
message.Complete();
471471
}
472+
473+
/// <summary>
474+
/// Moves the message from current queue to the dead letter queue
475+
/// </summary>
476+
/// <param name="logger"></param>
477+
/// <param name="id"></param>
478+
/// <param name="message"></param>
479+
/// <param name="description"></param>
480+
/// <param name="ex"></param>
481+
internal async void DeadLetterProcessedMessageAsync(
482+
ILogger logger,
483+
EventId id,
484+
IAmqpMessage message,
485+
string description,
486+
Exception ex = null)
487+
{
488+
if (message == null) throw new ArgumentNullException(nameof(message));
489+
490+
logger.LogWarning(id, ex, description);
491+
await message.RejectAsync();
492+
}
472493
/// <summary>
473494
/// Registers an alternate messaging factory
474495
/// </summary>

src/Helsenorge.Messaging/Amqp/AmqpMessage.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ public IAmqpMessage Clone(bool includePayload = true)
323323

324324
public async Task CompleteAsync() => await CompleteActionAsync.Invoke().ConfigureAwait(false);
325325

326-
public void Reject() => ReleaseAction.Invoke();
326+
public void Reject() => RejectAction.Invoke();
327327

328-
public async Task RejectAsync() => await ReleaseActionAsync.Invoke().ConfigureAwait(false);
328+
public async Task RejectAsync() => await RejectActionAsync.Invoke().ConfigureAwait(false);
329329

330330
public void Release() => ReleaseAction.Invoke();
331331

src/Helsenorge.Messaging/Amqp/Receivers/MessageListener.cs

+12-2
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ private async Task<IncomingMessage> HandleRawMessageAsync(IAmqpMessage message,
254254
stopwatch.Stop();
255255
return incomingMessage;
256256
}
257-
catch (CertificateException ex)
257+
// Network related issues when validating certificate (e.g. CRL offline) is considered temporary
258+
// This is why when we get validation error of type RevokedOffline then process it as unhandeled exception
259+
// Unhandeled exception will place it on dead letter (after given retries) and it can then be reprocesses when issue has been resolved
260+
catch (CertificateException ex) when (ex.Error != CertificateErrors.RevokedOffline)
258261
{
259262
Logger.LogWarning($"{ex.Description}. MessageFunction: {message.MessageFunction} " +
260263
$"FromHerId: {message.FromHerId} ToHerId: {message.ToHerId} CpaId: {message.CpaId} " +
@@ -263,7 +266,7 @@ private async Task<IncomingMessage> HandleRawMessageAsync(IAmqpMessage message,
263266
await AmqpCore.ReportErrorToExternalSenderAsync(Logger, ex.EventId, message, ex.ErrorCode, ex.Description, ex.AdditionalInformation).ConfigureAwait(false);
264267
await MessagingNotification.NotifyHandledExceptionAsync(message, ex).ConfigureAwait(false);
265268
}
266-
catch (SecurityException ex)
269+
catch (SecurityException ex) when (ex is not CertificateException)
267270
{
268271
await AmqpCore.ReportErrorToExternalSenderAsync(Logger, EventIds.RemoteCertificate, message, "transport:invalid-certificate", ex.Message, null, ex).ConfigureAwait(false);
269272
await MessagingNotification.NotifyHandledExceptionAsync(message, ex).ConfigureAwait(false);
@@ -493,6 +496,9 @@ private void ReportErrorOnRemoteCertificate(X509Certificate2 certificate,
493496
case CertificateErrors.RevokedUnknown:
494497
throw new CertificateException(error, "transport:revoked-certificate", "Unable to determine revocation status",
495498
EventIds.RemoteCertificateRevocation, AdditionalInformation(certificate));
499+
case CertificateErrors.RevokedOffline:
500+
throw new CertificateException(error, "transport:revoked-certificate-offline", "Unable to determine revocation status. Service is unavailable",
501+
EventIds.RemoteCertificateRevocationOffline, AdditionalInformation(certificate));
496502
default: // since the value is bit-coded
497503
throw new CertificateException(error, "transport:invalid-certificate", "More than one error with certificate",
498504
EventIds.RemoteCertificate, AdditionalInformation(certificate));
@@ -536,6 +542,10 @@ private void ReportErrorOnLocalCertificate(X509Certificate2 certificate, Certifi
536542
description = "Certificate is missing";
537543
id = EventIds.LocalCertificate;
538544
break;
545+
case CertificateErrors.RevokedOffline:
546+
description = "Unable to determine revocation status. Service is unavailable";
547+
id = EventIds.LocalCertificateRevocationOffline;
548+
break;
539549
default: // since the value is bit-coded
540550
description = "More than one error with certificate";
541551
id = EventIds.LocalCertificate;

src/Helsenorge.Registries/Abstractions/CertificateErrors.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ public enum CertificateErrors
3838
/// </summary>
3939
Revoked = 8,
4040
/// <summary>
41-
/// Unable to determine revocation status. Service may be unavailable
41+
/// Unable to determine revocation status
4242
/// </summary>
4343
RevokedUnknown = 16,
4444
/// <summary>
4545
/// The certificate is missing
4646
/// </summary>
47-
Missing = 32
47+
Missing = 32,
48+
/// <summary>
49+
/// Unable to determine revocation status. Service is unavailable
50+
/// </summary>
51+
RevokedOffline = 64
4852
}
4953
}

src/Helsenorge.Registries/CertificateValidator.cs

+26-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System;
1010
using System.Security.Cryptography.X509Certificates;
1111
using Helsenorge.Registries.Abstractions;
12+
using Helsenorge.Registries.Utilities;
1213

1314
namespace Helsenorge.Registries
1415
{
@@ -18,6 +19,7 @@ namespace Helsenorge.Registries
1819
public class CertificateValidator : ICertificateValidator
1920
{
2021
private readonly bool _useOnlineRevocationCheck;
22+
private readonly IX509Chain _chain;
2123

2224
/// <summary>
2325
/// CertificateValidator constructor
@@ -26,7 +28,20 @@ public class CertificateValidator : ICertificateValidator
2628
public CertificateValidator(bool useOnlineRevocationCheck = true)
2729
{
2830
_useOnlineRevocationCheck = useOnlineRevocationCheck;
31+
_chain = new X509ChainWrapper();
2932
}
33+
34+
/// <summary>
35+
/// CertificateValidator constructor
36+
/// </summary>
37+
/// <param name="chain">You can set your own X509Chain.</param>
38+
/// <param name="useOnlineRevocationCheck">Should online certificate revocation list be used. Optional, default true.</param>
39+
internal CertificateValidator(IX509Chain chain, bool useOnlineRevocationCheck = true)
40+
{
41+
_useOnlineRevocationCheck = useOnlineRevocationCheck;
42+
_chain = chain;
43+
}
44+
3045
/// <summary>
3146
/// Validates the provided certificate
3247
/// </summary>
@@ -49,30 +64,27 @@ public CertificateErrors Validate(X509Certificate2 certificate, X509KeyUsageFlag
4964
result |= CertificateErrors.EndDate;
5065
}
5166

52-
if(!certificate.HasKeyUsage(usage))
67+
if (!certificate.HasKeyUsage(usage))
5368
result |= CertificateErrors.Usage;
5469

55-
var chain = new X509Chain
56-
{
57-
ChainPolicy =
58-
{
59-
RevocationMode = _useOnlineRevocationCheck ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
60-
RevocationFlag = X509RevocationFlag.EntireChain,
61-
UrlRetrievalTimeout = TimeSpan.FromSeconds(30),
62-
VerificationTime = DateTime.Now,
63-
}
64-
};
6570

66-
using (chain)
71+
_chain.ChainPolicy.RevocationMode = _useOnlineRevocationCheck ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
72+
_chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
73+
_chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(30);
74+
_chain.ChainPolicy.VerificationTime = DateTime.Now;
75+
76+
using (_chain)
6777
{
68-
if (chain.Build(certificate)) return result;
78+
if (_chain.Build(certificate)) return result;
6979

70-
foreach (var status in chain.ChainStatus)
80+
foreach (var status in _chain.ChainStatus)
7181
{
7282
// ReSharper disable once SwitchStatementMissingSomeCases
7383
switch (status.Status)
7484
{
7585
case X509ChainStatusFlags.OfflineRevocation:
86+
result |= CertificateErrors.RevokedOffline;
87+
break;
7688
case X509ChainStatusFlags.RevocationStatusUnknown:
7789
result |= CertificateErrors.RevokedUnknown;
7890
break;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace Helsenorge.Registries.Utilities
5+
{
6+
internal interface IX509Chain : IDisposable
7+
{
8+
internal X509ChainPolicy ChainPolicy { get; }
9+
internal X509ChainStatus[] ChainStatus { get; }
10+
internal bool Build(X509Certificate2 certificate);
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Security.Cryptography.X509Certificates;
2+
3+
namespace Helsenorge.Registries.Utilities
4+
{
5+
internal class X509ChainWrapper : IX509Chain
6+
{
7+
private readonly X509Chain _chain;
8+
9+
internal X509ChainWrapper()
10+
{
11+
_chain = new X509Chain();
12+
}
13+
14+
public X509ChainPolicy ChainPolicy => _chain.ChainPolicy;
15+
16+
public X509ChainStatus[] ChainStatus => _chain.ChainStatus;
17+
18+
public bool Build(X509Certificate2 certificate)
19+
{
20+
return _chain.Build(certificate);
21+
}
22+
23+
public void Dispose()
24+
{
25+
_chain.Dispose();
26+
}
27+
}
28+
}

test/Helsenorge.Messaging.Tests.Mocks/MockFactory.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,20 @@ public MockCommunicationParty(MockFactory factory, int herId)
6666
Synchronous = new MockQueue($"{herId}_sync");
6767
Error = new MockQueue($"{herId}_error");
6868
SynchronousReply = new MockQueue($"{herId}_syncreply");
69-
69+
DeadLetter = new MockQueue($"{herId}_dl");
70+
7071
factory.Qeueues.Add(Asynchronous.Name, Asynchronous.Messages);
7172
factory.Qeueues.Add(Synchronous.Name, Synchronous.Messages);
7273
factory.Qeueues.Add(Error.Name, Error.Messages);
7374
factory.Qeueues.Add(SynchronousReply.Name, SynchronousReply.Messages);
75+
factory.Qeueues.Add(DeadLetter.Name, DeadLetter.Messages);
7476
}
7577

7678
public MockQueue Asynchronous { get; }
7779
public MockQueue Synchronous { get; }
7880
public MockQueue Error { get; }
7981
public MockQueue SynchronousReply { get; }
82+
public MockQueue DeadLetter { get; }
8083

8184
}
8285

test/Helsenorge.Messaging.Tests.Mocks/MockMessage.cs

+2-10
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,14 @@ public Task RelaseAsync()
7878

7979
public void Reject()
8080
{
81+
Queue.Remove(this);
82+
DeadLetterQueue.Add(this);
8183
}
8284

8385
public Task RejectAsync()
84-
{
85-
return Task.CompletedTask;
86-
}
87-
88-
public void DeadLetter()
8986
{
9087
Queue.Remove(this);
9188
DeadLetterQueue.Add(this);
92-
}
93-
94-
public Task DeadLetterAsync()
95-
{
96-
DeadLetter();
9789
return Task.CompletedTask;
9890
}
9991

test/Helsenorge.Messaging.Tests/Amqp/Receivers/AsynchronousReceiveTests.cs

+26-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ await RunAsynchronousReceive(
7171
messageModification: (m) => { });
7272
}
7373

74-
[TestMethod, TestCategory("X509Chain")]
74+
[TestMethod]
7575
public async Task Asynchronous_Receive_CertificateSignError()
7676
{
7777
Exception receiveException = null;
@@ -550,6 +550,29 @@ await RunAsynchronousReceive(
550550
},
551551
messageModification: (m) => { });
552552
}
553+
554+
[TestMethod]
555+
public async Task Asynchronous_Receive_RemoteCertificateOfflineRevocation()
556+
{
557+
CertificateValidator.SetError(
558+
(c, u) => (u == X509KeyUsageFlags.NonRepudiation) ? CertificateErrors.RevokedOffline : CertificateErrors.None);
559+
560+
await RunAsynchronousReceive(
561+
postValidation: () =>
562+
{
563+
Assert.AreEqual(1, MockFactory.Helsenorge.Asynchronous.Messages.Count);
564+
Assert.AreEqual(0, MockFactory.OtherParty.Error.Messages.Count);
565+
Assert.IsNotNull(MockLoggerProvider.FindEntry(EventIds.UnknownError));
566+
},
567+
wait: () => _unhandledExceptionCalled,
568+
received: (m) =>
569+
{
570+
Assert.IsTrue(m.SignatureError != CertificateErrors.None);
571+
return Task.CompletedTask;
572+
},
573+
messageModification: (m) => { });
574+
}
575+
553576
[TestMethod]
554577
public async Task Asynchronous_Receive_RemoteCertificateMultiple()
555578
{
@@ -788,6 +811,7 @@ private MockMessage CreateAsynchronousMessage()
788811
TimeToLive = TimeSpan.FromSeconds(15),
789812
ReplyTo = MockFactory.OtherParty.Asynchronous.Name,
790813
Queue = MockFactory.Helsenorge.Asynchronous.Messages,
814+
DeadLetterQueue = MockFactory.Helsenorge.DeadLetter.Messages
791815
};
792816
}
793817

@@ -810,6 +834,7 @@ private MockMessage CreateAsynchronousMessageProtected()
810834
TimeToLive = TimeSpan.FromSeconds(15),
811835
ReplyTo = MockFactory.OtherParty.Asynchronous.Name,
812836
Queue = MockFactory.Helsenorge.Asynchronous.Messages,
837+
DeadLetterQueue = MockFactory.Helsenorge.DeadLetter.Messages
813838
};
814839
}
815840
}

test/Helsenorge.Messaging.Tests/Amqp/Receivers/ErrorReceiveTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ private MockMessage CreateMockMessage(XDocument content = null)
138138
TimeToLive = TimeSpan.FromSeconds(15),
139139
ReplyTo = MockFactory.OtherParty.Synchronous.Name,
140140
Queue = MockFactory.Helsenorge.Error.Messages,
141+
DeadLetterQueue = MockFactory.Helsenorge.DeadLetter.Messages
141142
};
142143
}
143144
}

0 commit comments

Comments
 (0)