Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

Commit 2470c0a

Browse files
committed
#816 Fix save certificate race condition
1 parent 77662e8 commit 2470c0a

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

src/Titanium.Web.Proxy/Certificates/CertificateManager.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ private X509Certificate2 makeCertificate(string certificateName, bool isRootCert
412412
return certificate;
413413
}
414414

415+
private static ConcurrentDictionary<string, object> saveCertificateLocks
416+
= new ConcurrentDictionary<string, object>();
417+
415418
/// <summary>
416419
/// Create an SSL certificate
417420
/// </summary>
@@ -445,7 +448,21 @@ private X509Certificate2 makeCertificate(string certificateName, bool isRootCert
445448

446449
try
447450
{
448-
certificateCache.SaveCertificate(subjectName, certificate);
451+
//acquire lock by subjectName
452+
//Async lock is not needed. Since this is a rare race-condition
453+
lock (saveCertificateLocks.GetOrAdd(subjectName, new object()))
454+
{
455+
try
456+
{
457+
//no two tasks with same subject name should together enter here
458+
certificateCache.SaveCertificate(subjectName, certificate);
459+
}
460+
finally
461+
{
462+
//save operation is complete. Free lock from memory.
463+
saveCertificateLocks.TryRemove(subjectName, out var _);
464+
}
465+
}
449466
}
450467
catch (Exception e)
451468
{
@@ -482,13 +499,17 @@ private X509Certificate2 makeCertificate(string certificateName, bool isRootCert
482499
}
483500

484501
// handle burst requests with same certificate name
485-
// by checking for existing task for same certificate name
502+
// by checking for existing task for same certificate name.
503+
// If two or more requests hit this block at the same time, then multiple tasks will be created,
504+
// which is okay. That certificate will be created twice or more. We don't anticipate many requests for same host at the same time.
505+
// This saves us from another expensive lock. The goal here is to minimize creation of multiple tasks for same certification name.
506+
// Goal is not to guarantee single certificate creation, which is not needed in our case.
486507
if (pendingCertificateCreationTasks.TryGetValue(certificateName, out var task))
487508
{
488509
return await task;
489510
}
490511

491-
// run certificate creation task & add it to pending tasks
512+
// run certificate creation task & add it to pending tasks
492513
task = Task.Run(() =>
493514
{
494515
var result = CreateCertificate(certificateName, false);

tests/Titanium.Web.Proxy.UnitTests/CertificateManagerTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,31 @@ public async Task Simple_Create_Win_Certificate_Test()
7676
mgr.StopClearIdleCertificates();
7777

7878
}
79+
80+
[TestMethod]
81+
public async Task Create_Server_Certificate_Test()
82+
{
83+
var tasks = new List<Task>();
84+
85+
var mgr = new CertificateManager(null, null, false, false, false, new Lazy<ExceptionHandler>(() => (e =>
86+
{
87+
Debug.WriteLine(e.ToString());
88+
Debug.WriteLine(e.InnerException?.ToString());
89+
})).Value)
90+
{ CertificateEngine = CertificateEngine.BouncyCastleFast };
91+
92+
mgr.SaveFakeCertificates = true;
93+
94+
for (int i = 0; i < 500; i++)
95+
{
96+
tasks.AddRange(hostNames.Select(host => Task.Run(() =>
97+
{
98+
var certificate = mgr.CreateServerCertificate(host);
99+
Assert.IsNotNull(certificate);
100+
})));
101+
}
102+
103+
await Task.WhenAll(tasks.ToArray());
104+
}
79105
}
80106
}

0 commit comments

Comments
 (0)