Skip to content

Commit 321ec73

Browse files
feat: add X509Certificate2 certificate support for SSL connections in Sender
1 parent ae56498 commit 321ec73

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

src/dummy-http-server/DummyHttpServer.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
using System.Text;
3030
using FastEndpoints;
3131
using FastEndpoints.Security;
32+
using Microsoft.AspNetCore.Server.Kestrel.Https;
3233

3334
namespace dummy_http_server;
3435

@@ -42,7 +43,7 @@ public class DummyHttpServer : IDisposable
4243
private readonly TimeSpan? _withStartDelay;
4344

4445
public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, bool withRetriableError = false,
45-
bool withErrorMessage = false, TimeSpan? withStartDelay = null)
46+
bool withErrorMessage = false, TimeSpan? withStartDelay = null, bool requireClientCert = false)
4647
{
4748
var bld = WebApplication.CreateBuilder();
4849

@@ -71,6 +72,15 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
7172
bld.Services.AddHealthChecks();
7273
bld.WebHost.ConfigureKestrel(o =>
7374
{
75+
if (requireClientCert)
76+
{
77+
o.ConfigureHttpsDefaults(https =>
78+
{
79+
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
80+
https.AllowAnyClientCertificate();
81+
});
82+
}
83+
7484
o.Limits.MaxRequestBodySize = 1073741824;
7585
o.ListenLocalhost(29474,
7686
options => { options.UseHttps(); });
@@ -253,4 +263,4 @@ public string PrintBuffer()
253263
sb.Append(Encoding.UTF8.GetString(bytes, lastAppend, i - lastAppend));
254264
return sb.ToString();
255265
}
256-
}
266+
}

src/net-questdb-client-tests/HttpTests.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
******************************************************************************/
2525

2626

27+
using System.Security.Cryptography.X509Certificates;
2728
using System.Text;
2829
using dummy_http_server;
2930
using NUnit.Framework;
@@ -1624,4 +1625,47 @@ await sender.Table("table name")
16241625
// ReSharper disable once DisposeOnUsingVariable
16251626
srv.Dispose();
16261627
}
1627-
}
1628+
1629+
[Test]
1630+
public async Task SendWithCert()
1631+
{
1632+
#if NET9_0_OR_GREATER
1633+
using var cert = X509CertificateLoader.LoadPkcs12FromFile("certificate.pfx", null);
1634+
#else
1635+
using var cert = new X509Certificate2("certificate.pfx", (string?)null);
1636+
#endif
1637+
1638+
Assert.NotNull(cert);
1639+
1640+
using var server = new DummyHttpServer(requireClientCert: true);
1641+
await server.StartAsync(HttpsPort);
1642+
using var sender = Sender.Configure($"https::addr=localhost:{HttpsPort};tls_verify=unsafe_off;")
1643+
.WithClientCert(cert)
1644+
.Build();
1645+
1646+
await sender.Table("metrics")
1647+
.Symbol("tag", "value")
1648+
.Column("number", 12.2)
1649+
.AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
1650+
1651+
await sender.SendAsync();
1652+
Assert.That(
1653+
server.PrintBuffer(),
1654+
Is.EqualTo("metrics,tag=value number=12.2 1000000000\n"));
1655+
await server.StopAsync();
1656+
}
1657+
1658+
[Test]
1659+
public async Task FailsWhenExpectingCert()
1660+
{
1661+
using var server = new DummyHttpServer(requireClientCert: true);
1662+
await server.StartAsync(HttpsPort);
1663+
1664+
Assert.That(
1665+
() => Sender.Configure($"https::addr=localhost:{HttpsPort};tls_verify=unsafe_off;").Build(),
1666+
Throws.TypeOf<IngressError>().With.Message.Contains("ServerFlushError")
1667+
);
1668+
1669+
await server.StopAsync();
1670+
}
1671+
}

src/net-questdb-client/Senders/HttpSender.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ private void Build()
114114
_handler.SslOptions.ClientCertificates.Add(
115115
X509Certificate2.CreateFromPemFile(Options.tls_roots!, Options.tls_roots_password));
116116
}
117+
118+
if (Options.client_cert is not null)
119+
{
120+
_handler.SslOptions.ClientCertificates ??= new X509Certificate2Collection();
121+
_handler.SslOptions.ClientCertificates.Add(Options.client_cert);
122+
}
117123
}
118124

119125
_handler.ConnectTimeout = Options.auth_timeout;
@@ -620,4 +626,4 @@ public override void Dispose()
620626
Buffer.Clear();
621627
Buffer.TrimExcessBuffers();
622628
}
623-
}
629+
}

src/net-questdb-client/Senders/TcpSender.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
using System.Buffers.Text;
2828
using System.Net.Security;
2929
using System.Net.Sockets;
30+
using System.Security.Cryptography.X509Certificates;
3031
using QuestDB.Enums;
3132
using QuestDB.Utils;
3233
using ProtocolType = QuestDB.Enums.ProtocolType;
@@ -83,6 +84,12 @@ private void Build()
8384
Options.tls_verify == TlsVerifyType.unsafe_off ? AllowAllCertCallback : null,
8485
};
8586

87+
if (Options.client_cert is not null)
88+
{
89+
sslOptions.ClientCertificates ??= new X509CertificateCollection();
90+
sslOptions.ClientCertificates.Add(Options.client_cert);
91+
}
92+
8693
sslStream.AuthenticateAsClient(sslOptions);
8794
if (!sslStream.IsEncrypted)
8895
{
@@ -253,4 +260,4 @@ public override void Dispose()
253260
Buffer.Clear();
254261
Buffer.TrimExcessBuffers();
255262
}
256-
}
263+
}

src/net-questdb-client/Utils/SenderOptions.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using System.Data.Common;
2929
using System.Reflection;
3030
using System.Runtime.CompilerServices;
31+
using System.Security.Cryptography.X509Certificates;
3132
using System.Text.Json.Serialization;
3233
using QuestDB.Enums;
3334
using QuestDB.Senders;
@@ -81,6 +82,7 @@ public record SenderOptions
8182
private string? _tokenX;
8283
private string? _tokenY;
8384
private string? _username;
85+
private X509Certificate2? _clientCert;
8486

8587
/// <summary>
8688
/// Construct a <see cref="SenderOptions" /> object with default values.
@@ -473,6 +475,15 @@ public int Port
473475
}
474476
}
475477

478+
/// <summary>
479+
/// Specifies a client certificate to be used for TLS authentication.
480+
/// </summary>
481+
public X509Certificate2? client_cert
482+
{
483+
get => _clientCert;
484+
set => _clientCert = value;
485+
}
486+
476487
private void ParseIntWithDefault(string name, string defaultValue, out int field)
477488
{
478489
if (!int.TryParse(ReadOptionFromBuilder(name) ?? defaultValue, out field))
@@ -648,4 +659,17 @@ public ISender Build()
648659
{
649660
return Sender.New(this);
650661
}
651-
}
662+
663+
/// <summary>
664+
/// Sets a client certificate to be used for TLS authentication.
665+
/// </summary>
666+
/// <param name="cert"></param>
667+
/// <returns></returns>
668+
public SenderOptions WithClientCert(X509Certificate2 cert)
669+
{
670+
return this with
671+
{
672+
_clientCert = cert,
673+
};
674+
}
675+
}

0 commit comments

Comments
 (0)