diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml
index b3b6facd7..0aafeb43a 100644
--- a/.github/workflows/base.yml
+++ b/.github/workflows/base.yml
@@ -6,6 +6,10 @@ on:
docker-tag:
required: true
type: string
+ docker-registry:
+ required: false
+ type: string
+ default: ghcr.io/eventstore/eventstore
jobs:
test:
@@ -15,7 +19,9 @@ jobs:
matrix:
framework: [ net6.0, net7.0, net8.0 ]
os: [ ubuntu-latest ]
- test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ]
+ build: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ]
+ oss: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ]
+ ee: [ Plugins ]
configuration: [ release ]
runs-on: ${{ matrix.os }}
name: EventStore.Client.${{ matrix.test }}/${{ matrix.os }}/${{ matrix.framework }}/${{ inputs.docker-tag }}
@@ -25,10 +31,17 @@ jobs:
- shell: bash
run: |
git fetch --prune --unshallow
+ - name: Login to Cloudsmith
+ if: ${{ inputs.docker-registry != 'ghcr.io/eventstore/eventstore' }}
+ uses: docker/login-action@v3
+ with:
+ registry: docker.eventstore.com
+ username: ${{ secrets.CLOUDSMITH_CICD_USER }}
+ password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }}
- name: Pull EventStore Image
shell: bash
run: |
- docker pull ghcr.io/eventstore/eventstore:${{ inputs.docker-tag }}
+ docker pull ${{ inputs.docker-registry }}:${{ inputs.docker-tag }}
- name: Install dotnet SDKs
uses: actions/setup-dotnet@v3
with:
@@ -39,14 +52,27 @@ jobs:
- name: Compile
shell: bash
run: |
- dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.test }}
- - name: Run Tests
+ dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.build }}
+ - name: Run OSS Tests
+ if: ${{ inputs.docker-registry != '' }}
+ shell: bash
+ env:
+ ES_DOCKER_TAG: ${{ inputs.docker-tag }}
+ run: |
+ sudo ./gencert.sh
+ dotnet test --configuration ${{ matrix.configuration }} --blame \
+ --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \
+ --framework ${{ matrix.framework }} \
+ test/EventStore.Client.${{ matrix.oss }}.Tests
+ - name: Run EE Tests
+ if: ${{ inputs.docker-registry == '' }}
shell: bash
env:
ES_DOCKER_TAG: ${{ inputs.docker-tag }}
+ ES_DOCKER_REGISTRY: ${{ inputs.docker-registry }}
run: |
sudo ./gencert.sh
dotnet test --configuration ${{ matrix.configuration }} --blame \
--logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \
--framework ${{ matrix.framework }} \
- test/EventStore.Client.${{ matrix.test }}.Tests
+ test/EventStore.Client.${{ matrix.ee }}.Tests
diff --git a/.github/workflows/ee.yml b/.github/workflows/ee.yml
new file mode 100644
index 000000000..f8703993e
--- /dev/null
+++ b/.github/workflows/ee.yml
@@ -0,0 +1,16 @@
+name: Test EE
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+ tags:
+ - v*
+
+jobs:
+ test:
+ uses: ./.github/workflows/base.yml
+ with:
+ docker-tag: 24.2.0-jammy
+ docker-registry: docker.eventstore.com/eventstore-ee/eventstoredb-commercial
diff --git a/EventStore.Client.sln b/EventStore.Client.sln
index 51229f72c..03ecf4045 100644
--- a/EventStore.Client.sln
+++ b/EventStore.Client.sln
@@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManag
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Plugins.Tests", "test\EventStore.Client.Plugins.Tests\EventStore.Client.Plugins.Tests.csproj", "{7D929D45-F1D9-462B-BE49-84BEC11D5039}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -94,6 +96,10 @@ Global
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU
{E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU
+ {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.Build.0 = Debug|Any CPU
+ {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.ActiveCfg = Release|Any CPU
+ {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D3744A86-DD35-4104-AAEE-84B79062C4A2} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}
@@ -109,5 +115,6 @@ Global
{6CEB731F-72E1-461F-A6B3-54DBF3FD786C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
{22634CEE-4F7B-4679-A48D-38A2A8580ECA} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
{E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
+ {7D929D45-F1D9-462B-BE49-84BEC11D5039} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340}
EndGlobalSection
EndGlobal
diff --git a/gencert.ps1 b/gencert.ps1
index 3908f57e8..1101cc13d 100644
--- a/gencert.ps1
+++ b/gencert.ps1
@@ -4,18 +4,22 @@ Write-Host ">> Generating certificate..."
New-Item -ItemType Directory -Path .\certs -Force
# Set permissions for the directory
-icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
+icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F"
# Pull the Docker image
-docker pull eventstore/es-gencert-cli:1.0.2
+docker pull ghcr.io/eventstore/es-gencert-cli:1.3
-# Create CA certificate
-docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca
+docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca
-# Create node certificate
-docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
+docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
+
+# Create admin user
+docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin
+
+# Create an invalid user
+docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid
# Set permissions recursively for the directory
-icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
+icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F"
Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root
diff --git a/gencert.sh b/gencert.sh
index fa640f624..7cd69b56a 100755
--- a/gencert.sh
+++ b/gencert.sh
@@ -13,11 +13,15 @@ mkdir -p certs
chmod 0755 ./certs
-docker pull eventstore/es-gencert-cli:1.0.2
+docker pull ghcr.io/eventstore/es-gencert-cli:1.3
-docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca
+docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca
-docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
+docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost
+
+docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin
+
+docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid
chmod -R 0755 ./certs
diff --git a/src/EventStore.Client/CertificateUtils.cs b/src/EventStore.Client/CertificateUtils.cs
new file mode 100644
index 000000000..1cb5ba3a0
--- /dev/null
+++ b/src/EventStore.Client/CertificateUtils.cs
@@ -0,0 +1,107 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+#if NET48
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.Security;
+#endif
+
+namespace EventStore.Client;
+
+///
+/// Utility class for loading certificates and private keys from files.
+///
+static class CertificateUtils {
+ private static RSA LoadKey(string privateKeyPath) {
+ string[] allLines = File.ReadAllLines(privateKeyPath);
+ var header = allLines[0].Replace("-", "");
+ var privateKeyLines = allLines.Skip(1).Take(allLines.Length - 2);
+ var privateKey = Convert.FromBase64String(string.Join(string.Empty, privateKeyLines));
+
+ var rsa = RSA.Create();
+ switch (header) {
+ case "BEGIN PRIVATE KEY":
+#if NET
+ rsa.ImportPkcs8PrivateKey(new ReadOnlySpan(privateKey), out _);
+#else
+ {
+ var pemReader = new PemReader(new StringReader(string.Join(Environment.NewLine, allLines)));
+ var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
+ var privateKeyParams = (RsaPrivateCrtKeyParameters)keyPair.Private;
+ rsa.ImportParameters(DotNetUtilities.ToRSAParameters(privateKeyParams));
+ }
+#endif
+ break;
+
+ case "BEGIN RSA PRIVATE KEY":
+#if NET
+ rsa.ImportRSAPrivateKey(new ReadOnlySpan(privateKey), out _);
+#else
+ {
+ var pemReader = new PemReader(new StringReader(string.Join(Environment.NewLine, allLines)));
+ object pemObject = pemReader.ReadObject();
+ RsaPrivateCrtKeyParameters privateKeyParams;
+ if (pemObject is RsaPrivateCrtKeyParameters) {
+ privateKeyParams = (RsaPrivateCrtKeyParameters)pemObject;
+ } else if (pemObject is AsymmetricCipherKeyPair keyPair) {
+ privateKeyParams = (RsaPrivateCrtKeyParameters)keyPair.Private;
+ } else {
+ throw new NotSupportedException($"Unsupported PEM object type: {pemObject.GetType()}");
+ }
+
+ rsa.ImportParameters(DotNetUtilities.ToRSAParameters(privateKeyParams));
+ }
+#endif
+ break;
+
+ default:
+ rsa.Dispose();
+ throw new NotSupportedException($"Unsupported private key file format: {header}");
+ }
+
+ return rsa;
+ }
+
+ internal static X509Certificate2 LoadCertificate(string certificatePath) {
+ return new X509Certificate2(certificatePath);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static X509Certificate2 LoadFromFile(string certificatePath, string privateKeyPath) {
+ X509Certificate2? publicCertificate = null;
+ RSA? rsa = null;
+
+ try {
+ try {
+ publicCertificate = LoadCertificate(certificatePath);
+ } catch (Exception ex) {
+ throw new Exception($"Failed to load certificate: {ex.Message}");
+ }
+
+ try {
+ rsa = LoadKey(privateKeyPath);
+ } catch (Exception ex) {
+ throw new Exception($"Failed to load private key: {ex.Message}");
+ }
+
+ using var publicWithPrivate = publicCertificate.CopyWithPrivateKey(rsa);
+ var certificate = new X509Certificate2(publicWithPrivate.Export(X509ContentType.Pfx));
+
+ return certificate;
+ } finally {
+ publicCertificate?.Dispose();
+ rsa?.Dispose();
+ }
+ }
+}
diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs
index b28c3e0bb..3f6be47f7 100644
--- a/src/EventStore.Client/ChannelFactory.cs
+++ b/src/EventStore.Client/ChannelFactory.cs
@@ -1,6 +1,6 @@
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using Grpc.Net.Client;
-using System.Net.Security;
using EndPoint = System.Net.EndPoint;
using TChannel = Grpc.Net.Client.GrpcChannel;
@@ -25,7 +25,7 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint
DefaultRequestVersion = new Version(2, 0)
},
#else
- HttpHandler = CreateHandler(),
+ HttpHandler = CreateHandler(),
#endif
LoggerFactory = settings.LoggerFactory,
Credentials = settings.ChannelCredentials,
@@ -39,20 +39,16 @@ HttpMessageHandler CreateHandler() {
return settings.CreateHttpMessageHandler.Invoke();
}
- var configureClientCert = settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false };
+ var certificate = settings.ConnectivitySettings.ClientCertificate ??
+ settings.ConnectivitySettings.TlsCaFile;
+
+ var configureClientCert = settings.ConnectivitySettings is { Insecure: false } && certificate != null;
#if NET
var handler = new SocketsHttpHandler {
KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval,
KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout,
EnableMultipleHttp2Connections = true,
};
-
- if (configureClientCert)
- handler.SslOptions.ClientCertificates = [settings.ConnectivitySettings.TlsCaFile!];
-
- if (!settings.ConnectivitySettings.TlsVerifyCert) {
- handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
- }
#else
var handler = new WinHttpHandler {
TcpKeepAliveEnabled = true,
@@ -60,9 +56,20 @@ HttpMessageHandler CreateHandler() {
TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval,
EnableMultipleHttp2Connections = true
};
+#endif
+ if (settings.ConnectivitySettings.Insecure) return handler;
+#if NET
+ if (configureClientCert) {
+ handler.SslOptions.ClientCertificates = new X509CertificateCollection { certificate! };
+ }
- if (configureClientCert)
- handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile!);
+ if (!settings.ConnectivitySettings.TlsVerifyCert) {
+ handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+ }
+#else
+ if (configureClientCert) {
+ handler.ClientCertificates.Add(certificate!);
+ }
if (!settings.ConnectivitySettings.TlsVerifyCert) {
handler.ServerCertificateValidationCallback = delegate { return true; };
diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj
index ddf36c6af..094ed87fc 100644
--- a/src/EventStore.Client/EventStore.Client.csproj
+++ b/src/EventStore.Client/EventStore.Client.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs
index 57515d270..92890d2f2 100644
--- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs
+++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs
@@ -107,6 +107,11 @@ public bool Insecure {
///
public X509Certificate2? TlsCaFile { get; set; }
+ ///
+ /// Client certificate used for user authentication.
+ ///
+ public X509Certificate2? ClientCertificate { get; set; }
+
///
/// The default .
///
diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
index ccfd47dbf..253532aab 100644
--- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
+++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
@@ -41,6 +41,8 @@ private static class ConnectionStringParser {
private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure);
private const string KeepAliveInterval = nameof(KeepAliveInterval);
private const string KeepAliveTimeout = nameof(KeepAliveTimeout);
+ private const string CertPath = nameof(CertPath);
+ private const string CertKeyPath = nameof(CertKeyPath);
private const string UriSchemeDiscover = "esdb+discover";
@@ -62,6 +64,8 @@ private static class ConnectionStringParser {
{ ThrowOnAppendFailure, typeof(bool) },
{ KeepAliveInterval, typeof(int) },
{ KeepAliveTimeout, typeof(int) },
+ { CertPath, typeof(string)},
+ { CertKeyPath, typeof(string)},
};
public static EventStoreClientSettings Parse(string connectionString) {
@@ -80,14 +84,9 @@ public static EventStoreClientSettings Parse(string connectionString) {
currentIndex = userInfoIndex + UserInfoSeparator.Length;
}
- var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal);
- var questionMarkIndex = connectionString.IndexOf(
- QuestionMark,
- Math.Max(currentIndex, slashIndex),
- StringComparison.Ordinal
- );
-
- var endIndex = connectionString.Length;
+ var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal);
+ var questionMarkIndex = connectionString.IndexOf(QuestionMark, currentIndex, StringComparison.Ordinal);
+ var endIndex = connectionString.Length;
//for simpler substring operations:
if (slashIndex == -1) slashIndex = int.MaxValue;
@@ -228,25 +227,39 @@ private static EventStoreClientSettings CreateSettings(
}
}
+ var certPathSet = typedOptions.TryGetValue(CertPath, out var certPath);
+ var certKeyPathSet = typedOptions.TryGetValue(CertKeyPath, out var certKeyPath);
+
+ if (certPathSet ^ certKeyPathSet)
+ throw new InvalidClientCertificateException(
+ $"Invalid certificate settings. {nameof(CertPath)} and {nameof(CertKeyPath)} must both be set"
+ );
+
+ if (certPathSet && certKeyPathSet) {
+ try {
+ settings.ConnectivitySettings.ClientCertificate =
+ CertificateUtils.LoadFromFile((string)certPath!, (string)certKeyPath!);
+ } catch (Exception ex) {
+ throw new InvalidClientCertificateException($"Invalid certificate settings. {ex.Message}");
+ }
+ }
+
settings.CreateHttpMessageHandler = CreateDefaultHandler;
return settings;
HttpMessageHandler CreateDefaultHandler() {
- var configureClientCert = settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false };
+ var certificate = settings.ConnectivitySettings.ClientCertificate ??
+ settings.ConnectivitySettings.TlsCaFile;
+
+ var configureClientCert = settings.ConnectivitySettings is { Insecure: false } && certificate != null;
+
#if NET
var handler = new SocketsHttpHandler {
KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval,
KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout,
EnableMultipleHttp2Connections = true,
};
-
- if (configureClientCert)
- handler.SslOptions.ClientCertificates = [settings.ConnectivitySettings.TlsCaFile!];
-
- if (!settings.ConnectivitySettings.TlsVerifyCert) {
- handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
- }
#else
var handler = new WinHttpHandler {
TcpKeepAliveEnabled = true,
@@ -254,9 +267,20 @@ HttpMessageHandler CreateDefaultHandler() {
TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval,
EnableMultipleHttp2Connections = true
};
+#endif
+ if (settings.ConnectivitySettings.Insecure) return handler;
+#if NET
+ if (configureClientCert) {
+ handler.SslOptions.ClientCertificates = [certificate!];
+ }
- if (configureClientCert)
- handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile!);
+ if (!settings.ConnectivitySettings.TlsVerifyCert) {
+ handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+ }
+#else
+ if (configureClientCert) {
+ handler.ClientCertificates.Add(certificate!);
+ }
if (!settings.ConnectivitySettings.TlsVerifyCert) {
handler.ServerCertificateValidationCallback = delegate { return true; };
diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs
index 294242cb0..44a72cf7e 100644
--- a/src/EventStore.Client/HttpFallback.cs
+++ b/src/EventStore.Client/HttpFallback.cs
@@ -1,6 +1,7 @@
using System;
using System.Net;
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -12,10 +13,10 @@ internal class HttpFallback : IDisposable {
private readonly UserCredentials? _defaultCredentials;
private readonly string _addressScheme;
- internal HttpFallback (EventStoreClientSettings settings) {
- _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme;
- _defaultCredentials = settings.DefaultCredentials;
-
+ internal HttpFallback(EventStoreClientSettings settings) {
+ _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme;
+ _defaultCredentials = settings.DefaultCredentials;
+
var handler = new HttpClientHandler();
if (!settings.ConnectivitySettings.Insecure) {
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
@@ -23,17 +24,20 @@ internal HttpFallback (EventStoreClientSettings settings) {
if (settings.ConnectivitySettings.TlsCaFile != null)
handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile);
+ if (settings.ConnectivitySettings.ClientCertificate != null)
+ handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate);
+
if (!settings.ConnectivitySettings.TlsVerifyCert)
- handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
+ handler.ServerCertificateCustomValidationCallback = delegate { return true; };
}
_httpClient = new HttpClient(handler);
if (settings.DefaultDeadline.HasValue) {
_httpClient.Timeout = settings.DefaultDeadline.Value;
}
-
+
_jsonSettings = new JsonSerializerOptions {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
diff --git a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj
new file mode 100644
index 000000000..ee7d48512
--- /dev/null
+++ b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/test/EventStore.Client.Plugins.Tests/client_certificate.cs b/test/EventStore.Client.Plugins.Tests/client_certificate.cs
new file mode 100644
index 000000000..b1d220c0e
--- /dev/null
+++ b/test/EventStore.Client.Plugins.Tests/client_certificate.cs
@@ -0,0 +1,88 @@
+namespace EventStore.Client.Plugins.Tests;
+
+[Trait("Category", "Target:Plugins")]
+[Trait("Category", "Type:UserCertificate")]
+public class client_certificate(ITestOutputHelper output, EventStoreFixture fixture)
+ : EventStoreTests(output, fixture) {
+ public static IEnumerable