diff --git a/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Halibut.TestUtils.CompatBinary.SchannelProbe.csproj b/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Halibut.TestUtils.CompatBinary.SchannelProbe.csproj
new file mode 100644
index 000000000..e5382f162
--- /dev/null
+++ b/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Halibut.TestUtils.CompatBinary.SchannelProbe.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ 9.0
+ Halibut.TestUtils.SampleProgram.SchannelProbe
+ enable
+ true
+
+
+
+ net48;net8.0
+
+
+ net8.0
+
+
+
+
+
+
+
+
diff --git a/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Program.cs b/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Program.cs
new file mode 100644
index 000000000..8adbeee15
--- /dev/null
+++ b/source/Halibut.TestUtils.CompatBinary.SchannelProbe/Program.cs
@@ -0,0 +1,137 @@
+using System;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Halibut;
+using Halibut.Diagnostics;
+using Halibut.ServiceModel;
+using Halibut.TestUtils.Contracts;
+
+namespace Halibut.TestUtils.SampleProgram.SchannelProbe
+{
+ public class Program
+ {
+ public static async Task Main()
+ {
+ using var cts = new CancellationTokenSource(GetTestTimeout());
+ using var _ = cts.Token.Register(() => Environment.Exit(-10060));
+
+ var mode = GetSetting("mode");
+ Console.WriteLine($"Mode is: {mode}");
+
+ if (mode.Equals("serviceonly", StringComparison.OrdinalIgnoreCase))
+ {
+ await RunExternalService(cts.Token);
+ }
+ else
+ {
+ Console.WriteLine($"Unknown mode: {mode}");
+ throw new Exception($"Unknown mode: {mode}");
+ }
+
+ return 1;
+ }
+
+ static async Task RunExternalService(CancellationToken cancellationToken)
+ {
+ var serviceCert = new X509Certificate2(GetSetting("tentaclecertpath"));
+ var octopusThumbprint = GetSetting("octopusthumbprint");
+ var serviceConnectionType = ParseServiceConnectionType(GetSetting("ServiceConnectionType"));
+
+ var services = new DelegateServiceFactory();
+ services.Register(() => new SayHelloServiceImpl());
+
+ using var tentacle = new HalibutRuntimeBuilder()
+ .WithServiceFactory(services)
+ .WithServerCertificate(serviceCert)
+ .WithLogFactory(new LogFactory())
+ .Build();
+
+ switch (serviceConnectionType)
+ {
+ case ServiceConnectionType.Polling:
+ var addressToPoll = GetSetting("octopusservercommsport");
+ tentacle.Poll(
+ new Uri("poll://SQ-TENTAPOLL"),
+ new ServiceEndPoint(new Uri(addressToPoll), octopusThumbprint, null, new HalibutTimeoutsAndLimits()),
+ cancellationToken);
+ break;
+ case ServiceConnectionType.Listening:
+ var port = tentacle.Listen();
+ Console.WriteLine($"Listening on port: {port}");
+ tentacle.Trust(octopusThumbprint);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(serviceConnectionType));
+ }
+
+ Console.WriteLine("RunningAndReady");
+ await Console.Out.FlushAsync();
+ await WaitUntilSignaledToDie();
+ }
+
+ static async Task WaitUntilSignaledToDie()
+ {
+ var stayAliveFile = GetSetting("CompatBinaryStayAliveFilePath");
+ while (true)
+ {
+ try
+ {
+ using (new FileStream(stayAliveFile, FileMode.Open, FileAccess.Read, FileShare.None))
+ {
+ }
+
+ try
+ {
+ File.Delete(stayAliveFile);
+ }
+ finally
+ {
+ Environment.Exit(0);
+ }
+ }
+ catch (Exception)
+ {
+ }
+
+ if (!File.Exists(stayAliveFile))
+ {
+ Environment.Exit(0);
+ }
+
+ await Task.Delay(2000);
+ }
+ }
+
+ static ServiceConnectionType ParseServiceConnectionType(string s)
+ {
+ if (Enum.TryParse(s, out ServiceConnectionType result))
+ return result;
+ throw new Exception($"Unknown service connection type '{s}'");
+ }
+
+ static TimeSpan GetTestTimeout()
+ {
+ var timeoutString = GetSetting("TestTimeout");
+ return string.IsNullOrWhiteSpace(timeoutString) ? TimeSpan.FromMinutes(15) : TimeSpan.Parse(timeoutString);
+ }
+
+ static string GetSetting(string name) => Environment.GetEnvironmentVariable(name) ?? string.Empty;
+ }
+
+ enum ServiceConnectionType
+ {
+ Polling,
+ Listening
+ }
+
+ class SayHelloServiceImpl : IAsyncSayHelloService
+ {
+ public async Task SayHelloAsync(string name, CancellationToken cancellationToken)
+ {
+ await Task.CompletedTask;
+ return name + "...";
+ }
+ }
+}
diff --git a/source/Halibut.TestUtils.Contracts/ISayHelloService.cs b/source/Halibut.TestUtils.Contracts/ISayHelloService.cs
new file mode 100644
index 000000000..76ad6aee7
--- /dev/null
+++ b/source/Halibut.TestUtils.Contracts/ISayHelloService.cs
@@ -0,0 +1,15 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Halibut.TestUtils.Contracts
+{
+ public interface ISayHelloService
+ {
+ string SayHello(string name);
+ }
+
+ public interface IAsyncSayHelloService
+ {
+ Task SayHelloAsync(string name, CancellationToken cancellationToken);
+ }
+}
diff --git a/source/Halibut.Tests.DotMemory/MemoryFixture.cs b/source/Halibut.Tests.DotMemory/MemoryFixture.cs
index ca55418ab..7f4c1cf3d 100644
--- a/source/Halibut.Tests.DotMemory/MemoryFixture.cs
+++ b/source/Halibut.Tests.DotMemory/MemoryFixture.cs
@@ -59,11 +59,26 @@ public void TcpClientsAreDisposedCorrectly()
.WriteTo.NUnitOutput()
.CreateLogger();
+ // Two separate HalibutRuntime instances are used to avoid an SChannel session cache
+ // collision on Windows (.NET Framework / SslProtocols.None). SChannel's TLS session
+ // cache is per-process and keyed on certificate + host. If a single runtime acts as
+ // both a TLS server (accepting inbound connections) and a TLS client (making outbound
+ // polling connections) using the same certificate, SChannel can incorrectly reuse a
+ // server-side session for a client-side handshake, causing SSPI/TLS failures.
+ //
+ // - server: pure TLS server — accepts inbound connections from listening tentacles.
+ // Uses Certificates.Octopus.
+ // - pollingServer: pure TLS client — only makes outbound polling connections to tentacles.
+ // Must use a DIFFERENT certificate (Certificates.TentacleListening) so
+ // that SChannel never sees the same cert in both server and client roles
+ // within this process.
HalibutRuntime? server = null;
+ HalibutRuntime? pollingServer = null;
try
{
server = RunServer(Certificates.Octopus, out var port);
+ pollingServer = RunPollingServer(Certificates.TentacleListening);
var expectedTcpClientCount = 1; //server listen = 1 tcpclient
//valid requests
@@ -75,13 +90,15 @@ public void TcpClientsAreDisposedCorrectly()
for (var i = 0; i < NumberOfClients; i++)
{
expectedTcpClientCount++; // each time the server polls, it keeps a tcpclient (as we dont have support to say StopPolling)
- RunPollingClient(server, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint).GetAwaiter().GetResult();
+ RunPollingClient(pollingServer, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint).GetAwaiter().GetResult();
}
#if SUPPORTS_WEB_SOCKET_CLIENT
+ //setup polling websocket
+ AddSslCertToLocalStoreAndRegisterFor("0.0.0.0:8434");
for (var i = 0; i < NumberOfClients; i++)
{
- RunWebSocketPollingClient(server, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint, Certificates.OctopusPublicThumbprint).GetAwaiter().GetResult();
+ RunWebSocketPollingClient(pollingServer, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint, Certificates.TentacleListeningPublicThumbprint).GetAwaiter().GetResult();
}
#endif
@@ -106,6 +123,7 @@ public void TcpClientsAreDisposedCorrectly()
finally
{
server?.DisposeAsync().GetAwaiter().GetResult();
+ pollingServer?.DisposeAsync().GetAwaiter().GetResult();
}
}
@@ -142,16 +160,33 @@ static HalibutRuntime RunServer(X509Certificate2 serverCertificate, out int port
.WithLogFactory(new TestContextLogFactory("client", LogLevel.Info))
.Build();
- //set up listening
+ // Trust the listening tentacle certificate for inbound connections.
+ // This runtime only accepts connections — it never makes outbound polling connections —
+ // keeping it in a pure TLS server role (see declaration comment above).
server.Trust(Certificates.TentacleListeningPublicThumbprint);
port = server.Listen();
- //setup polling websocket
- AddSslCertToLocalStoreAndRegisterFor("0.0.0.0:8434");
-
return server;
}
+ // pollingServer intentionally uses Certificates.TentacleListening rather than
+ // Certificates.Octopus (which server uses). This keeps the two certificates in distinct
+ // TLS roles within this process: Octopus is used only as a TLS server cert (by server),
+ // and TentacleListening is used only as a TLS client cert (here, and in RunListeningClient).
+ // Using the same cert in both roles would trigger an SChannel session-cache collision on
+ // Windows with SslProtocols.None (see declaration comment above).
+ static HalibutRuntime RunPollingServer(X509Certificate2 serverCertificate)
+ {
+ var services = new DelegateServiceFactory();
+ services.Register(() => new AsyncCalculatorService());
+
+ return new HalibutRuntimeBuilder()
+ .WithServerCertificate(serverCertificate)
+ .WithServiceFactory(services)
+ .WithLogFactory(new TestContextLogFactory("polling-server", LogLevel.Info))
+ .Build();
+ }
+
static async Task RunListeningClient(X509Certificate2 clientCertificate, int port, string remoteThumbprint, bool expectSuccess = true)
{
await using (var runtime = new HalibutRuntimeBuilder().WithServerCertificate(clientCertificate).Build())
@@ -169,9 +204,13 @@ static async Task RunPollingClient(HalibutRuntime server, X509Certificate2 clien
.Build())
{
runtime.Listen(new IPEndPoint(IPAddress.IPv6Any, 8433));
- runtime.Trust(Certificates.OctopusPublicThumbprint);
+ // Trust the thumbprint of pollingServer's certificate (TentacleListening), which is
+ // the cert pollingServer presents when it dials in to establish the polling connection.
+ runtime.Trust(Certificates.TentacleListeningPublicThumbprint);
//setup polling
+ // The remote thumbprint here is this runtime's own certificate (TentaclePolling),
+ // which pollingServer verifies when it connects to port 8433.
var serverEndpoint = new ServiceEndPoint(new Uri("https://localhost:8433"), Certificates.TentaclePollingPublicThumbprint, runtime.TimeoutsAndLimits)
{
TcpClientConnectTimeout = TimeSpan.FromSeconds(5)
diff --git a/source/Halibut.Tests/BadCertificatesTests.cs b/source/Halibut.Tests/BadCertificatesTests.cs
index 00ee8e3aa..4801bf7b0 100644
--- a/source/Halibut.Tests/BadCertificatesTests.cs
+++ b/source/Halibut.Tests/BadCertificatesTests.cs
@@ -30,6 +30,7 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI
var clientTrustProvider = new DefaultTrustProvider();
var unauthorizedThumbprint = "";
var firstCall = true;
+ var serviceThumbprint = "";
var unauthorizedClientHasConnected = new TaskCompletionSource();
CancellationToken.Register(() => unauthorizedClientHasConnected.TrySetCanceled()); // backup to fail the test in case it never connects
@@ -43,7 +44,7 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI
{
if (firstCall)
{
- clientTrustProvider.IsTrusted(CertAndThumbprint.TentaclePolling.Thumbprint).Should().BeFalse();
+ clientTrustProvider.IsTrusted(serviceThumbprint).Should().BeFalse();
firstCall = false;
}
@@ -53,6 +54,8 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI
})
.Build(CancellationToken))
{
+ serviceThumbprint = clientAndBuilder.ServiceThumbprint;
+
// Act
var clientCountingService = clientAndBuilder.CreateAsyncClient();
await clientCountingService.IncrementAsync();
@@ -62,8 +65,8 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI
// Assert
countingService.CurrentValue().Should().Be(1);
- clientTrustProvider.IsTrusted(CertAndThumbprint.TentaclePolling.Thumbprint).Should().BeTrue();
- unauthorizedThumbprint.Should().Be(CertAndThumbprint.TentaclePolling.Thumbprint);
+ clientTrustProvider.IsTrusted(serviceThumbprint).Should().BeTrue();
+ unauthorizedThumbprint.Should().Be(serviceThumbprint);
}
}
@@ -93,6 +96,8 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon
})
.Build(CancellationToken))
{
+ var serviceThumbprint = clientAndBuilder.ServiceThumbprint;
+
using var cts = new CancellationTokenSource();
var clientCountingService = clientAndBuilder.CreateAsyncClient(point =>
{
@@ -105,7 +110,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon
// Interestingly the message exchange error is logged to a non polling looking URL, perhaps because it has not been identified?
Wait.UntilActionSucceeds(() => {
AllLogs(serviceLoggers).Select(l => l.FormattedMessage).ToArray()
- .Should().Contain(s => s.Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint '4098EC3A2FC2B92B97339D3831BA230CC1DD590F' which is not in the list of thumbprints that we trust"));
+ .Should().Contain(s => s.Contains($"and attempted a message exchange, but it presented a client certificate with the thumbprint '{serviceThumbprint}' which is not in the list of thumbprints that we trust"));
},
TimeSpan.FromSeconds(10),
Logger,
@@ -124,7 +129,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon
// Assert
countingService.CurrentValue().Should().Be(0, "With a bad certificate the request never should have been made");
- unauthorizedThumbprint.Should().Be(CertAndThumbprint.TentaclePolling.Thumbprint);
+ unauthorizedThumbprint.Should().Be(serviceThumbprint);
}
}
@@ -195,8 +200,8 @@ public async Task FailWhenClientPresentsWrongCertificateToListeningService(Clien
serviceLoggers[serviceLoggers.Keys.First(x => x != nameof(MessageSerializer))].GetLogs().Should()
.Contain(log => log.FormattedMessage
- .Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint " +
- "'76225C0717A16C1D0BA4A7FFA76519D286D8A248' which is not in the list of thumbprints that we trust"));
+ .Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint")
+ && log.FormattedMessage.Contains("which is not in the list of thumbprints that we trust"));
}
}
@@ -254,11 +259,13 @@ public async Task FailWhenListeningServicePresentsWrongCertificate(ClientAndServ
.WithCountingService(countingService)
.Build(CancellationToken))
{
+ var serviceThumbprint = clientAndBuilder.ServiceThumbprint;
+
var clientCountingService = clientAndBuilder.CreateAsyncClient();
(await AssertionExtensions.Should(() => clientCountingService.IncrementAsync()).ThrowAsync())
.And.Message.Should().Contain("" +
"We expected the server to present a certificate with the thumbprint 'EC32122053C6BFF582F8246F5697633D06F0F97F'. " +
- "Instead, it presented a certificate with a thumbprint of '36F35047CE8B000CF4C671819A2DD1AFCDE3403D'");
+ $"Instead, it presented a certificate with a thumbprint of '{serviceThumbprint}'");
countingService.CurrentValue().Should().Be(0, "With a bad certificate the request never should have been made");
}
}
@@ -275,6 +282,8 @@ public async Task FailWhenPollingServicePresentsWrongCertificate(ClientAndServic
.RecordingClientLogs(out var serviceLoggers)
.Build(CancellationToken))
{
+ var serviceThumbprint = clientAndBuilder.ServiceThumbprint;
+
using var cts = new CancellationTokenSource();
var clientCountingService = clientAndBuilder.CreateAsyncClient(point =>
{
@@ -285,7 +294,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate(ClientAndServic
// Interestingly the message exchange error is logged to a non polling looking URL, perhaps because it has not been identified?
Wait.UntilActionSucceeds(() => { AllLogs(serviceLoggers).Select(l => l.FormattedMessage).ToArray()
- .Should().Contain(s => s.Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint '4098EC3A2FC2B92B97339D3831BA230CC1DD590F' which is not in the list of thumbprints that we trust")); },
+ .Should().Contain(s => s.Contains($"and attempted a message exchange, but it presented a client certificate with the thumbprint '{serviceThumbprint}' which is not in the list of thumbprints that we trust")); },
TimeSpan.FromSeconds(10),
Logger,
CancellationToken);
diff --git a/source/Halibut.Tests/ClientServerLifecycleTests.cs b/source/Halibut.Tests/ClientServerLifecycleTests.cs
index 730e4bf98..c7ac17df0 100644
--- a/source/Halibut.Tests/ClientServerLifecycleTests.cs
+++ b/source/Halibut.Tests/ClientServerLifecycleTests.cs
@@ -25,13 +25,33 @@ namespace Halibut.Tests
{
public class ClientServerLifecycleTests : BaseTest
{
+ TmpDirectory? tmpDirectory;
+ CertAndThumbprint serverCert = null!;
+ CertAndThumbprint listenerCert = null!;
+ CertAndThumbprint pollerCert = null!;
+
+ [SetUp]
+ public void SetUpCerts()
+ {
+ tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ serverCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ listenerCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, tmpDirectory);
+ pollerCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, tmpDirectory);
+ }
+
+ [TearDown]
+ public void TearDownCerts()
+ {
+ tmpDirectory?.Dispose();
+ }
+
[Test]
public async Task ListeningConfiguration()
{
await using var server = RunServer(out var serverPort);
await using var runtime = CreateRuntimeForListener();
- var client = CreateClient(runtime, serverPort);
+ var client = CreateClient(runtime, serverPort, serverCert);
var result = await client.AddAsync(2, 2);
result.Should().Be(4);
}
@@ -56,7 +76,7 @@ public async Task ListeningThenPollingConfiguration()
HalibutRuntime CreateRuntimeForListener()
{
var runtime = new HalibutRuntimeBuilder()
- .WithServerCertificate(Certificates.TentacleListening)
+ .WithServerCertificate(listenerCert.Certificate2)
.WithLogFactory(new TestLogFactory(HalibutLog))
.Build();
return runtime;
@@ -65,15 +85,15 @@ HalibutRuntime CreateRuntimeForListener()
HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncClientCalculatorService client)
{
var runtime = new HalibutRuntimeBuilder()
- .WithServerCertificate(Certificates.TentaclePolling)
+ .WithServerCertificate(pollerCert.Certificate2)
.WithLogFactory(new TestLogFactory(HalibutLog))
.Build();
var port = runtime.Listen();
- runtime.Trust(Certificates.OctopusPublicThumbprint);
+ runtime.Trust(serverCert.Thumbprint);
var pollEndpoint = new ServiceEndPoint(
baseUri: new Uri($"https://localhost:{port}/"),
- remoteThumbprint: Certificates.TentaclePollingPublicThumbprint,
+ remoteThumbprint: pollerCert.Thumbprint,
halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits
)
{
@@ -83,7 +103,7 @@ HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncCl
serverRuntime.Poll(pollingUri, pollEndpoint, CancellationToken);
var clientEndpoint = new ServiceEndPoint(
baseUri: pollingUri,
- remoteThumbprint: Certificates.OctopusPublicThumbprint,
+ remoteThumbprint: serverCert.Thumbprint,
halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits
);
client = runtime.CreateAsyncClient(clientEndpoint);
@@ -91,11 +111,11 @@ HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncCl
return runtime;
}
- static IAsyncClientCalculatorService CreateClient(HalibutRuntime runtime, int port)
+ static IAsyncClientCalculatorService CreateClient(HalibutRuntime runtime, int port, CertAndThumbprint serverCertAndThumbprint)
{
var endpoint = new ServiceEndPoint(
baseUri: $"https://localhost:{port}",
- remoteThumbprint: Certificates.OctopusPublicThumbprint,
+ remoteThumbprint: serverCertAndThumbprint.Thumbprint,
halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits
);
var client = runtime
@@ -115,13 +135,13 @@ HalibutRuntime RunServer(out int port)
var services = CreateServiceFactory();
var runtime = new HalibutRuntimeBuilder()
- .WithServerCertificate(Certificates.Octopus)
+ .WithServerCertificate(serverCert.Certificate2)
.WithServiceFactory(services)
.WithLogFactory(new TestLogFactory(HalibutLog))
.Build();
- runtime.Trust(Certificates.TentacleListeningPublicThumbprint);
- runtime.Trust(Certificates.TentaclePollingPublicThumbprint);
+ runtime.Trust(listenerCert.Thumbprint);
+ runtime.Trust(pollerCert.Thumbprint);
port = runtime.Listen();
return runtime;
diff --git a/source/Halibut.Tests/Halibut.Tests.csproj b/source/Halibut.Tests/Halibut.Tests.csproj
index 602280ad8..d5d3051aa 100644
--- a/source/Halibut.Tests/Halibut.Tests.csproj
+++ b/source/Halibut.Tests/Halibut.Tests.csproj
@@ -31,6 +31,7 @@
+
diff --git a/source/Halibut.Tests/SchannelSessionCacheFixture.cs b/source/Halibut.Tests/SchannelSessionCacheFixture.cs
new file mode 100644
index 000000000..a26a964bb
--- /dev/null
+++ b/source/Halibut.Tests/SchannelSessionCacheFixture.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Halibut.Diagnostics.LogCreators;
+using Halibut.Tests.Support;
+using Halibut.Tests.Support.BackwardsCompatibility;
+using Halibut.Tests.Support.Logging;
+using Halibut.Tests.TestServices.Async;
+using Halibut.Tests.Util;
+using Halibut.TestUtils.Contracts;
+using NUnit.Framework;
+
+namespace Halibut.Tests
+{
+ ///
+ /// Proves that process isolation prevents SChannel session cache collisions.
+ ///
+ /// In-process, a single acting as both TLS server and TLS
+ /// client to localhost with two different certificates can collide in the SChannel session
+ /// cache (Windows). Running the tentacle in a separate process avoids this because the
+ /// SChannel session cache is per-process.
+ ///
+ /// These tests verify that two separate processes — one listening tentacle and one polling
+ /// tentacle — both using the same certificate and both connecting via localhost, can
+ /// simultaneously communicate successfully with an in-process Octopus server.
+ ///
+ [TestFixture]
+ [NonParallelizable]
+ public class SchannelSessionCacheFixture : BaseTest
+ {
+ [Test]
+ public async Task ListeningTentacleInSeparateProcessCanCommunicateWithOctopus()
+ {
+ using var tmpDirectory = new TmpDirectory();
+ var octopusCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+ var tentacleCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+
+ var octopus = new HalibutRuntimeBuilder()
+ .WithServerCertificate(octopusCert.Certificate2)
+ .WithLogFactory(new TestContextLogCreator("Octopus", Logging.LogLevel.Trace).ToCachingLogFactory())
+ .WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build())
+ .Build();
+
+ await using var _ = new AsyncDisposableAction(async () => await octopus.DisposeAsync());
+
+ octopus.Trust(tentacleCert.Thumbprint);
+
+ using var runningTentacle = await new SchannelProbeBinaryRunner(
+ ServiceConnectionType.Listening,
+ clientListenPort: null,
+ clientCertAndThumbprint: octopusCert,
+ serviceCertAndThumbprint: tentacleCert,
+ logger: Logger).Run();
+
+ runningTentacle.ServiceListenPort.Should().NotBeNull("listening tentacle should have reported its port");
+
+ var serviceUri = new Uri($"https://localhost:{runningTentacle.ServiceListenPort}");
+ var serviceEndPoint = new ServiceEndPoint(serviceUri, tentacleCert.Thumbprint, octopus.TimeoutsAndLimits);
+
+ var echo = octopus.CreateAsyncClient(serviceEndPoint);
+ var result = await echo.SayHelloAsync("world");
+
+ result.Should().Be("world...");
+ }
+
+ [Test]
+ public async Task PollingTentacleInSeparateProcessCanCommunicateWithOctopus()
+ {
+ using var tmpDirectory = new TmpDirectory();
+ var octopusCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+ var tentacleCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+
+ var octopus = new HalibutRuntimeBuilder()
+ .WithServerCertificate(octopusCert.Certificate2)
+ .WithLogFactory(new TestContextLogCreator("Octopus", Logging.LogLevel.Trace).ToCachingLogFactory())
+ .WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build())
+ .Build();
+
+ await using var _ = new AsyncDisposableAction(async () => await octopus.DisposeAsync());
+
+ octopus.Trust(tentacleCert.Thumbprint);
+ var pollingListenPort = octopus.Listen();
+
+ using var runningTentacle = await new SchannelProbeBinaryRunner(
+ ServiceConnectionType.Polling,
+ clientListenPort: pollingListenPort,
+ clientCertAndThumbprint: octopusCert,
+ serviceCertAndThumbprint: tentacleCert,
+ logger: Logger).Run();
+
+ var serviceUri = new Uri("poll://SQ-TENTAPOLL");
+ var serviceEndPoint = new ServiceEndPoint(serviceUri, tentacleCert.Thumbprint, octopus.TimeoutsAndLimits);
+
+ var echo = octopus.CreateAsyncClient(serviceEndPoint);
+ var result = await echo.SayHelloAsync("world");
+
+ result.Should().Be("world...");
+ }
+
+ [Test]
+ public async Task ListeningAndPollingTentaclesInSeparateProcessesCanSimultaneouslyCommunicateWithOctopus()
+ {
+ using var tmpDirectory = new TmpDirectory();
+ var octopusCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+ // Both tentacles intentionally share the same certificate to maximise the chance of
+ // triggering an SChannel session-cache collision if process isolation were absent.
+ var sharedTentacleCert = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath);
+
+ var octopus = new HalibutRuntimeBuilder()
+ .WithServerCertificate(octopusCert.Certificate2)
+ .WithLogFactory(new TestContextLogCreator("Octopus", Logging.LogLevel.Trace).ToCachingLogFactory())
+ .WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build())
+ .Build();
+
+ await using var _ = new AsyncDisposableAction(async () => await octopus.DisposeAsync());
+
+ octopus.Trust(sharedTentacleCert.Thumbprint);
+ var pollingListenPort = octopus.Listen();
+
+ // Start listening tentacle
+ using var listeningTentacle = await new SchannelProbeBinaryRunner(
+ ServiceConnectionType.Listening,
+ clientListenPort: null,
+ clientCertAndThumbprint: octopusCert,
+ serviceCertAndThumbprint: sharedTentacleCert,
+ logger: Logger).Run();
+
+ listeningTentacle.ServiceListenPort.Should().NotBeNull("listening tentacle should have reported its port");
+
+ // Start polling tentacle
+ using var pollingTentacle = await new SchannelProbeBinaryRunner(
+ ServiceConnectionType.Polling,
+ clientListenPort: pollingListenPort,
+ clientCertAndThumbprint: octopusCert,
+ serviceCertAndThumbprint: sharedTentacleCert,
+ logger: Logger).Run();
+
+ var listeningServiceUri = new Uri($"https://localhost:{listeningTentacle.ServiceListenPort}");
+ var listeningEndPoint = new ServiceEndPoint(listeningServiceUri, sharedTentacleCert.Thumbprint, octopus.TimeoutsAndLimits);
+
+ var pollingServiceUri = new Uri("poll://SQ-TENTAPOLL");
+ var pollingEndPoint = new ServiceEndPoint(pollingServiceUri, sharedTentacleCert.Thumbprint, octopus.TimeoutsAndLimits);
+
+ var listeningEcho = octopus.CreateAsyncClient(listeningEndPoint);
+ var pollingEcho = octopus.CreateAsyncClient(pollingEndPoint);
+
+ // Call both simultaneously
+ var listeningTask = listeningEcho.SayHelloAsync("from-listening");
+ var pollingTask = pollingEcho.SayHelloAsync("from-polling");
+
+ var results = await Task.WhenAll(listeningTask, pollingTask);
+
+ results[0].Should().Be("from-listening...");
+ results[1].Should().Be("from-polling...");
+ }
+ }
+
+ class AsyncDisposableAction : IAsyncDisposable
+ {
+ readonly Func action;
+
+ public AsyncDisposableAction(Func action)
+ {
+ this.action = action;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await action();
+ }
+ }
+}
diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs
index 3860e7173..b7a0675ce 100644
--- a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs
+++ b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs
@@ -9,15 +9,25 @@ public class HalibutTestBinaryPath
public string BinPath(string version)
{
var onDiskVersion = version.Replace(".", "_");
+ var projectName = $"Halibut.TestUtils.CompatBinary.v{onDiskVersion}";
+ return ResolveProjectBinPath(projectName);
+ }
+
+ public string SchannelProbeBinPath()
+ {
+ return ResolveProjectBinPath("Halibut.TestUtils.CompatBinary.SchannelProbe");
+ }
+
+ string ResolveProjectBinPath(string projectName)
+ {
var assemblyDir = new DirectoryInfo(Path.GetDirectoryName(typeof(HalibutTestBinaryRunner).Assembly.Location)!);
var upAt = assemblyDir.Parent!.Parent!.Parent!.Parent!;
- var projectName = $"Halibut.TestUtils.CompatBinary.v{onDiskVersion}";
var executable = Path.Combine(upAt.FullName, projectName, assemblyDir.Parent.Parent.Name, assemblyDir.Parent.Name, assemblyDir.Name, projectName);
executable = AddExeForWindows(executable);
if (!File.Exists(executable))
{
- throw new Exception("Could not executable at path:\n" +
+ throw new Exception("Could not find executable at path:\n" +
executable + "\n" +
$"Did you forget to update the csproj to depend on {projectName}\n" +
"If testing a previously untested version of Halibut a new project may be required.");
@@ -25,7 +35,7 @@ public string BinPath(string version)
return executable;
}
-
+
string AddExeForWindows(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return path + ".exe";
diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs
index 563ec4133..859f65996 100644
--- a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs
+++ b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs
@@ -17,7 +17,8 @@ public class LatestClientAndPreviousServiceVersionBuilder : IClientAndServiceBui
{
readonly ServiceConnectionType serviceConnectionType;
CertAndThumbprint serviceCertAndThumbprint;
- CertAndThumbprint clientCertAndThumbprint = CertAndThumbprint.Octopus;
+ CertAndThumbprint clientCertAndThumbprint;
+ TmpDirectory? tmpDirectory;
Version? version;
Func? portForwarderFactory;
Reference? portForwarderReference;
@@ -27,10 +28,11 @@ public class LatestClientAndPreviousServiceVersionBuilder : IClientAndServiceBui
ConcurrentDictionary? clientInMemoryLoggers;
readonly OldServiceAvailableServices availableServices = new(false, false);
- LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint)
+ LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint, CertAndThumbprint clientCertAndThumbprint)
{
this.serviceConnectionType = serviceConnectionType;
this.serviceCertAndThumbprint = serviceCertAndThumbprint;
+ this.clientCertAndThumbprint = clientCertAndThumbprint;
}
public LatestClientAndPreviousServiceVersionBuilder WithCertificates(
@@ -44,17 +46,23 @@ public LatestClientAndPreviousServiceVersionBuilder WithCertificates(
public static LatestClientAndPreviousServiceVersionBuilder WithPollingService()
{
- return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling, clientCert) { tmpDirectory = tmpDirectory };
}
public static LatestClientAndPreviousServiceVersionBuilder WithPollingOverWebSocketService()
{
- return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling, clientCert) { tmpDirectory = tmpDirectory };
}
public static LatestClientAndPreviousServiceVersionBuilder WithListeningService()
{
- return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening, clientCert) { tmpDirectory = tmpDirectory };
}
public static LatestClientAndPreviousServiceVersionBuilder ForServiceConnectionType(ServiceConnectionType connectionType)
@@ -287,7 +295,7 @@ public async Task Build(CancellationToken cancellationToken)
portForwarderReference.Value = portForwarder;
}
- return new ClientAndService(client, runningOldHalibutBinary, serviceUri, serviceCertAndThumbprint, portForwarder, disposableCollection, proxy, proxyDetails, cancellationTokenSource);
+ return new ClientAndService(client, runningOldHalibutBinary, serviceUri, serviceCertAndThumbprint, portForwarder, disposableCollection, proxy, proxyDetails, cancellationTokenSource, tmpDirectory);
}
public class ClientAndService : IClientAndService
@@ -300,6 +308,7 @@ public class ClientAndService : IClientAndService
readonly CancellationTokenSource cancellationTokenSource;
readonly PortForwarder? portForwarder;
readonly HttpProxyService? httpProxy;
+ readonly TmpDirectory? tmpDirectory;
public ClientAndService(
HalibutRuntime client,
@@ -310,7 +319,8 @@ public ClientAndService(
DisposableCollection disposableCollection,
HttpProxyService? httpProxy,
ProxyDetails? proxyDetails,
- CancellationTokenSource cancellationTokenSource)
+ CancellationTokenSource cancellationTokenSource,
+ TmpDirectory? tmpDirectory)
{
Client = client;
this.runningOldHalibutBinary = runningOldHalibutBinary;
@@ -321,6 +331,7 @@ public ClientAndService(
this.disposableCollection = disposableCollection;
this.proxyDetails = proxyDetails;
this.cancellationTokenSource = cancellationTokenSource;
+ this.tmpDirectory = tmpDirectory;
}
public HalibutRuntime Client { get; }
@@ -356,6 +367,7 @@ public async ValueTask DisposeAsync()
Try.CatchingError(() => portForwarder?.Dispose(), LogError);
Try.CatchingError(() => disposableCollection.Dispose(), LogError);
Try.CatchingError(() => cancellationTokenSource.Dispose(), LogError);
+ Try.CatchingError(() => tmpDirectory?.Dispose(), LogError);
}
}
diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs
index ab03a3a01..4d581dfd5 100644
--- a/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs
+++ b/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs
@@ -28,7 +28,8 @@ public class PreviousClientVersionAndLatestServiceBuilder: IClientAndServiceBuil
readonly ServiceFactoryBuilder serviceFactoryBuilder = new();
readonly CertAndThumbprint serviceCertAndThumbprint;
- readonly CertAndThumbprint clientCertAndThumbprint = CertAndThumbprint.Octopus;
+ readonly CertAndThumbprint clientCertAndThumbprint;
+ TmpDirectory? tmpDirectory;
Version? version;
ProxyFactory? proxyFactory;
Reference? proxyServiceReference;
@@ -36,25 +37,32 @@ public class PreviousClientVersionAndLatestServiceBuilder: IClientAndServiceBuil
Reference? portForwarderReference;
LogLevel halibutLogLevel = LogLevel.Trace;
- PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint)
+ PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint, CertAndThumbprint clientCertAndThumbprint)
{
this.serviceConnectionType = serviceConnectionType;
this.serviceCertAndThumbprint = serviceCertAndThumbprint;
+ this.clientCertAndThumbprint = clientCertAndThumbprint;
}
public static PreviousClientVersionAndLatestServiceBuilder WithPollingService()
{
- return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling, clientCert) { tmpDirectory = tmpDirectory };
}
public static PreviousClientVersionAndLatestServiceBuilder WithPollingOverWebSocketsService()
{
- return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling, clientCert) { tmpDirectory = tmpDirectory };
}
public static PreviousClientVersionAndLatestServiceBuilder WithListeningService()
{
- return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening, clientCert) { tmpDirectory = tmpDirectory };
}
public static PreviousClientVersionAndLatestServiceBuilder ForServiceConnectionType(ServiceConnectionType connectionType)
@@ -312,7 +320,7 @@ public async Task Build(CancellationToken cancellationToken)
portForwarderReference.Value = portForwarder;
}
- return new ClientAndService(proxyClient, runningOldHalibutBinary, serviceUri, serviceCertAndThumbprint, service, disposableCollection, cancellationTokenSource, portForwarder, httpProxy, logger);
+ return new ClientAndService(proxyClient, runningOldHalibutBinary, serviceUri, serviceCertAndThumbprint, service, disposableCollection, cancellationTokenSource, portForwarder, httpProxy, logger, tmpDirectory);
}
public class ClientAndService : IClientAndService
@@ -326,6 +334,7 @@ public class ClientAndService : IClientAndService
readonly ILogger logger;
readonly PortForwarder? portForwarder;
readonly HttpProxyService? httpProxy;
+ readonly TmpDirectory? tmpDirectory;
public ClientAndService(
HalibutRuntime proxyClient,
@@ -337,7 +346,8 @@ public ClientAndService(
CancellationTokenSource cancellationTokenSource,
PortForwarder? portForwarder,
HttpProxyService? httpProxy,
- ILogger logger)
+ ILogger logger,
+ TmpDirectory? tmpDirectory)
{
Client = proxyClient;
this.httpProxy = httpProxy;
@@ -349,6 +359,7 @@ public ClientAndService(
this.disposableCollection = disposableCollection;
this.cancellationTokenSource = cancellationTokenSource;
this.logger = logger.ForContext();;
+ this.tmpDirectory = tmpDirectory;
}
///
@@ -386,6 +397,7 @@ public async ValueTask DisposeAsync()
Try.CatchingError(() => portForwarder?.Dispose(), LogError);
Try.CatchingError(disposableCollection.Dispose, LogError); ;
Try.CatchingError(() => cancellationTokenSource.Dispose(), LogError);
+ Try.CatchingError(() => tmpDirectory?.Dispose(), LogError);
}
}
}
diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/SchannelProbeBinaryRunner.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/SchannelProbeBinaryRunner.cs
new file mode 100644
index 000000000..ea0248fe7
--- /dev/null
+++ b/source/Halibut.Tests/Support/BackwardsCompatibility/SchannelProbeBinaryRunner.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using CliWrap;
+using Halibut.Logging;
+using Serilog;
+
+namespace Halibut.Tests.Support.BackwardsCompatibility
+{
+ public class SchannelProbeBinaryRunner
+ {
+ readonly ServiceConnectionType serviceConnectionType;
+ readonly int? clientListenPort;
+ readonly CertAndThumbprint clientCertAndThumbprint;
+ readonly CertAndThumbprint serviceCertAndThumbprint;
+ readonly ILogger logger;
+
+ ///
+ /// Launches the SchannelProbe binary as a listening tentacle (server dials it) or a
+ /// polling tentacle (it dials the server). Uses the current version of Halibut.
+ ///
+ public SchannelProbeBinaryRunner(
+ ServiceConnectionType serviceConnectionType,
+ int? clientListenPort,
+ CertAndThumbprint clientCertAndThumbprint,
+ CertAndThumbprint serviceCertAndThumbprint,
+ ILogger logger)
+ {
+ this.serviceConnectionType = serviceConnectionType;
+ this.clientListenPort = clientListenPort;
+ this.clientCertAndThumbprint = clientCertAndThumbprint;
+ this.serviceCertAndThumbprint = serviceCertAndThumbprint;
+ this.logger = logger.ForContext();
+ }
+
+ public async Task Run()
+ {
+ var compatBinaryStayAlive = new CompatBinaryStayAlive(logger);
+
+ var settings = new Dictionary
+ {
+ { "mode", "serviceonly" },
+ { "tentaclecertpath", serviceCertAndThumbprint.CertificatePfxPath },
+ { "octopusthumbprint", clientCertAndThumbprint.Thumbprint },
+ { "halibutloglevel", LogLevel.Info.ToString() },
+ { CompatBinaryStayAlive.StayAliveFilePathEnvVarKey, compatBinaryStayAlive.LockFile },
+ { "WithStandardServices", true.ToString() },
+ { "WithCachingService", false.ToString() },
+ { "WithTentacleServices", false.ToString() },
+ { "ServiceConnectionType", serviceConnectionType.ToString() },
+ };
+
+ if (serviceConnectionType == ServiceConnectionType.Polling && clientListenPort.HasValue)
+ settings.Add("octopusservercommsport", "https://localhost:" + clientListenPort.Value);
+
+ var cts = new CancellationTokenSource();
+ var hasStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ int? serviceListenPort = null;
+
+ var runningTask = Task.Run(async () =>
+ {
+ try
+ {
+ await Cli.Wrap(new HalibutTestBinaryPath().SchannelProbeBinPath())
+ .WithEnvironmentVariables(settings)
+ .WithStandardOutputPipe(PipeTarget.ToDelegate((line, _) =>
+ {
+ logger.Information(line);
+ if (line.StartsWith("Listening on port: "))
+ serviceListenPort = int.Parse(Regex.Match(line, @"\d+").Value);
+ if (line.Contains("RunningAndReady"))
+ hasStarted.TrySetResult(true);
+ return Task.CompletedTask;
+ }))
+ .WithStandardErrorPipe(PipeTarget.ToDelegate((line, _) =>
+ {
+ logger.Information(line);
+ return Task.CompletedTask;
+ }))
+ .ExecuteAsync(cts.Token);
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception e)
+ {
+ hasStarted.TrySetException(e);
+ throw;
+ }
+ });
+
+ var winner = await Task.WhenAny(runningTask, hasStarted.Task, Task.Delay(TimeSpan.FromSeconds(30)));
+
+ if (winner == runningTask || !hasStarted.Task.IsCompleted)
+ {
+ cts.Cancel();
+ cts.Dispose();
+ compatBinaryStayAlive.Dispose();
+ if (winner == runningTask) await runningTask; // re-throw startup exception
+ throw new Exception("SchannelProbe binary did not start within 30 seconds");
+ }
+
+ return new RunningSchannelProbe(cts, serviceListenPort, compatBinaryStayAlive);
+ }
+
+ public class RunningSchannelProbe : IDisposable
+ {
+ readonly CancellationTokenSource cts;
+ readonly CompatBinaryStayAlive compatBinaryStayAlive;
+
+ public int? ServiceListenPort { get; }
+
+ public RunningSchannelProbe(CancellationTokenSource cts, int? serviceListenPort, CompatBinaryStayAlive compatBinaryStayAlive)
+ {
+ this.cts = cts;
+ this.compatBinaryStayAlive = compatBinaryStayAlive;
+ ServiceListenPort = serviceListenPort;
+ }
+
+ public void Dispose()
+ {
+ cts.Cancel();
+ cts.Dispose();
+ compatBinaryStayAlive.Dispose();
+ }
+ }
+ }
+}
diff --git a/source/Halibut.Tests/Support/IClient.cs b/source/Halibut.Tests/Support/IClient.cs
index c0c1b3824..c11ee8061 100644
--- a/source/Halibut.Tests/Support/IClient.cs
+++ b/source/Halibut.Tests/Support/IClient.cs
@@ -6,6 +6,7 @@ public interface IClient : IAsyncDisposable
{
HalibutRuntime Client { get; }
Uri? ListeningUri { get; }
+ string ClientThumbprint { get; }
TAsyncClientService CreateClient(Uri serviceEndPoint);
TAsyncClientService CreateClientWithoutService();
TAsyncClientService CreateClientWithoutService(Action modifyServiceEndpoint);
diff --git a/source/Halibut.Tests/Support/LatestClient.cs b/source/Halibut.Tests/Support/LatestClient.cs
index d632e10f2..05994e170 100644
--- a/source/Halibut.Tests/Support/LatestClient.cs
+++ b/source/Halibut.Tests/Support/LatestClient.cs
@@ -20,6 +20,7 @@ public LatestClient(
HalibutRuntime client,
Uri? listeningUri,
string thumbprint,
+ string clientThumbprint,
PortForwarder? portForwarder,
ProxyDetails? proxyDetails,
ServiceConnectionType serviceConnectionType,
@@ -28,6 +29,7 @@ public LatestClient(
Client = client;
ListeningUri = listeningUri;
this.thumbprint = thumbprint;
+ ClientThumbprint = clientThumbprint;
this.portForwarder = portForwarder;
this.proxyDetails = proxyDetails;
this.serviceConnectionType = serviceConnectionType;
@@ -37,6 +39,9 @@ public LatestClient(
public HalibutRuntime Client { get; }
public Uri? ListeningUri { get; }
+ /// The thumbprint of this client's own certificate (what a polling service must trust).
+ public string ClientThumbprint { get; }
+
public TAsyncClientService CreateClient(Uri serviceUri)
{
var serviceEndPoint = GetServiceEndPoint(serviceUri);
diff --git a/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs b/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs
index b9f043b71..46f9db137 100644
--- a/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs
+++ b/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs
@@ -31,6 +31,8 @@ public class LatestClientAndLatestServiceBuilder : IClientAndServiceBuilder
readonly LatestClientBuilder clientBuilder;
readonly LatestServiceBuilder serviceBuilder;
+ TmpDirectory? tmpDirectory;
+
ProxyFactory? proxyFactory;
Reference? proxyServiceReference;
@@ -52,17 +54,34 @@ public LatestClientAndLatestServiceBuilder(
public static LatestClientAndLatestServiceBuilder Polling(PollingQueueTestCase pollingQueueTestCase)
{
- return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.Octopus, CertAndThumbprint.TentaclePolling, pollingQueueTestCase);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, tmpDirectory);
+ var builder = new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Polling, clientCert, serviceCert, pollingQueueTestCase);
+ builder.tmpDirectory = tmpDirectory;
+ return builder;
}
public static LatestClientAndLatestServiceBuilder PollingOverWebSocket(PollingQueueTestCase pollingQueueTestCase)
{
- return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.Ssl, CertAndThumbprint.TentaclePolling, pollingQueueTestCase);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ // For WebSocket, the client cert must be CertAndThumbprint.Ssl because it is bound to the port
+ // via netsh http add sslcert and must match the cert registered in the Windows local machine cert store.
+ var clientCert = CertAndThumbprint.Ssl;
+ var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, tmpDirectory);
+ var builder = new LatestClientAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, clientCert, serviceCert, pollingQueueTestCase);
+ builder.tmpDirectory = tmpDirectory;
+ return builder;
}
public static LatestClientAndLatestServiceBuilder Listening()
{
- return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.Octopus, CertAndThumbprint.TentacleListening, null);
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, tmpDirectory);
+ var builder = new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Listening, clientCert, serviceCert, null);
+ builder.tmpDirectory = tmpDirectory;
+ return builder;
}
public static LatestClientAndLatestServiceBuilder ForServiceConnectionType(ServiceConnectionType serviceConnectionType, PollingQueueTestCase? pollingQueueTestCase = null)
@@ -366,7 +385,7 @@ public async Task Build(CancellationToken cancellationToken)
portForwarderReference.Value = portForwarder;
}
}
- return new ClientAndService(client, service, httpProxy);
+ return new ClientAndService(client, service, httpProxy, tmpDirectory, serviceBuilder.ServiceCertAndThumbprint.Thumbprint);
}
public class ClientAndService : IClientAndService
@@ -374,14 +393,19 @@ public class ClientAndService : IClientAndService
readonly LatestClient client;
readonly LatestService service;
readonly HttpProxyService? httpProxy;
+ readonly TmpDirectory? tmpDirectory;
public ClientAndService(
LatestClient client,
LatestService service,
- HttpProxyService? proxy)
+ HttpProxyService? proxy,
+ TmpDirectory? tmpDirectory,
+ string serviceThumbprint)
{
this.client = client;
this.service = service;
+ this.tmpDirectory = tmpDirectory;
+ ServiceThumbprint = serviceThumbprint;
httpProxy = proxy;
}
@@ -390,6 +414,14 @@ public ClientAndService(
public HalibutRuntime Client => client.Client;
public HalibutRuntime Service => service.Service;
+ ///
+ /// The actual thumbprint of the certificate the service is presenting.
+ /// Use this instead of .RemoteThumbprint when verifying
+ /// the service cert, as RemoteThumbprint reflects what the client is configured to trust
+ /// (which may differ, e.g. in bad-certificate tests).
+ ///
+ public string ServiceThumbprint { get; }
+
public ServiceEndPoint GetServiceEndPoint()
{
return client.GetServiceEndPoint(ServiceUri);
@@ -419,6 +451,7 @@ public async ValueTask DisposeAsync()
void LogError(Exception e) => logger.Warning(e, "Ignoring error in dispose");
Try.CatchingError(() => httpProxy?.Dispose(), LogError);
+ Try.CatchingError(() => tmpDirectory?.Dispose(), LogError);
}
}
}
diff --git a/source/Halibut.Tests/Support/LatestClientBuilder.cs b/source/Halibut.Tests/Support/LatestClientBuilder.cs
index 1de31111f..d43966063 100644
--- a/source/Halibut.Tests/Support/LatestClientBuilder.cs
+++ b/source/Halibut.Tests/Support/LatestClientBuilder.cs
@@ -24,6 +24,8 @@ public class LatestClientBuilder : IClientBuilder
CertAndThumbprint clientCertAndThumbprint;
readonly PollingQueueTestCase? pollingQueueTestCase;
+ TmpDirectory? tmpDirectory;
+
string clientTrustsThumbprint;
bool clientTrustsNoThumbprints;
IRpcObserver? clientRpcObserver;
@@ -65,10 +67,20 @@ public static LatestClientBuilder ForServiceConnectionType(ServiceConnectionType
switch (serviceConnectionType)
{
case ServiceConnectionType.Polling:
- return new LatestClientBuilder(ServiceConnectionType.Polling, CertAndThumbprint.Octopus, CertAndThumbprint.TentaclePolling, pollingQueueTestCase);
+ {
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new LatestClientBuilder(ServiceConnectionType.Polling, clientCert, CertAndThumbprint.TentaclePolling, pollingQueueTestCase) { tmpDirectory = tmpDirectory };
+ }
case ServiceConnectionType.Listening:
- return new LatestClientBuilder(ServiceConnectionType.Listening, CertAndThumbprint.Octopus, CertAndThumbprint.TentacleListening, pollingQueueTestCase);
+ {
+ var tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+ return new LatestClientBuilder(ServiceConnectionType.Listening, clientCert, CertAndThumbprint.TentacleListening, pollingQueueTestCase) { tmpDirectory = tmpDirectory };
+ }
case ServiceConnectionType.PollingOverWebSocket:
+ // For WebSocket, the client cert must be CertAndThumbprint.Ssl because it is bound to the port
+ // via netsh http add sslcert and must match the cert registered in the Windows local machine cert store.
return new LatestClientBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.Ssl, CertAndThumbprint.TentaclePolling, pollingQueueTestCase);
default:
throw new ArgumentOutOfRangeException(nameof(serviceConnectionType), serviceConnectionType, null);
@@ -228,6 +240,11 @@ public async Task Build(CancellationToken cancellationToken)
}
var disposableCollection = new DisposableCollection();
+ if (tmpDirectory is not null)
+ {
+ disposableCollection.Add(tmpDirectory);
+ }
+
PortForwarder? portForwarder = null;
Uri? clientListeningUri = null;
@@ -267,7 +284,7 @@ public async Task Build(CancellationToken cancellationToken)
portForwarderReference.Value = portForwarder;
}
- return new LatestClient(client, clientListeningUri, clientTrustsThumbprint, portForwarder, proxyDetails, serviceConnectionType, disposableCollection);
+ return new LatestClient(client, clientListeningUri, clientTrustsThumbprint, clientCertAndThumbprint.Thumbprint, portForwarder, proxyDetails, serviceConnectionType, disposableCollection);
}
IPendingRequestQueueFactory CreatePendingRequestQueueFactory(QueueMessageSerializer queueMessageSerializer, ILogFactory octopusLogFactory)
diff --git a/source/Halibut.Tests/Support/LatestServiceBuilder.cs b/source/Halibut.Tests/Support/LatestServiceBuilder.cs
index 19caa44b5..df92e43d6 100644
--- a/source/Halibut.Tests/Support/LatestServiceBuilder.cs
+++ b/source/Halibut.Tests/Support/LatestServiceBuilder.cs
@@ -31,7 +31,7 @@ public class LatestServiceBuilder : IServiceBuilder
IServiceFactory? serviceFactory;
string serviceTrustsThumbprint;
- readonly List listeningClientUris = new();
+ readonly List<(Uri ListeningUri, string? Thumbprint)> listeningClients = new();
Func? portForwarderFactory;
Reference? portForwarderReference;
Func? pollingReconnectRetryPolicy;
@@ -69,20 +69,27 @@ public static LatestServiceBuilder ForServiceConnectionType(ServiceConnectionTyp
}
}
- public LatestServiceBuilder WithListeningClient(Uri listeningClient)
+ public LatestServiceBuilder WithListeningClient(Uri listeningClientUri)
{
- listeningClientUris.Add(listeningClient);
+ // No explicit thumbprint: the service dials this client using its configured
+ // serviceTrustsThumbprint (which the wrong-certificate test helpers manipulate).
+ listeningClients.Add((listeningClientUri, null));
return this;
}
- public LatestServiceBuilder WithListeningClients(IEnumerable listeningClientUris)
+ public LatestServiceBuilder WithListeningClients(IEnumerable<(Uri ListeningUri, string Thumbprint)> listeningClients)
{
- this.listeningClientUris.AddRange(listeningClientUris);
+ foreach (var listeningClient in listeningClients)
+ {
+ this.listeningClients.Add((listeningClient.ListeningUri, listeningClient.Thumbprint));
+ }
return this;
}
+ public CertAndThumbprint ServiceCertAndThumbprint => serviceCertAndThumbprint;
+
public LatestServiceBuilder WithCertificate(CertAndThumbprint serviceCertAndThumbprint)
{
this.serviceCertAndThumbprint = serviceCertAndThumbprint;
@@ -220,13 +227,13 @@ public async Task Build(CancellationToken cancellationToken)
{
serviceUri = PollingTentacleServiceUri;
- foreach (var listeningClientUri in listeningClientUris)
+ foreach (var (listeningClientUri, clientThumbprint) in listeningClients)
{
for (var i = 0; i < pollingConnectionCount; i++)
{
service.Poll(
serviceUri,
- new ServiceEndPoint(listeningClientUri, serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits),
+ new ServiceEndPoint(listeningClientUri, clientThumbprint ?? serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits),
cancellationToken);
}
}
@@ -235,11 +242,11 @@ public async Task Build(CancellationToken cancellationToken)
{
serviceUri = PollingOverWebSocketTentacleServiceUri;
- foreach (var listeningClientUri in listeningClientUris)
+ foreach (var (listeningClientUri, clientThumbprint) in listeningClients)
{
service.Poll(
serviceUri,
- new ServiceEndPoint(listeningClientUri, serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits),
+ new ServiceEndPoint(listeningClientUri, clientThumbprint ?? serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits),
cancellationToken);
}
}
diff --git a/source/Halibut.Tests/Support/TestCertificates.cs b/source/Halibut.Tests/Support/TestCertificates.cs
new file mode 100644
index 000000000..1bf296c70
--- /dev/null
+++ b/source/Halibut.Tests/Support/TestCertificates.cs
@@ -0,0 +1,51 @@
+#if NETFRAMEWORK
+using Halibut.Tests.Util;
+#endif
+
+namespace Halibut.Tests.Support
+{
+ ///
+ /// Decides which certificate a test builder should use, based on the target framework.
+ ///
+ /// On .NET Framework 4.8, causes Windows
+ /// SChannel to use a per-process TLS session cache keyed on certificate + hostname. Reusing the same static
+ /// certificates across tests in one process causes incorrect session reuse when connecting to localhost,
+ /// producing AuthenticationExceptions. To avoid this we generate a fresh, unique certificate per test.
+ ///
+ /// On other frameworks the static certificates are safe to reuse and, crucially, sharing them enables
+ /// SChannel TLS session resumption (fast resumed handshakes). Generating unique certs there would defeat
+ /// resumption and slow down every handshake, which can break tests that enforce short receive timeouts.
+ ///
+ /// All of the #if NETFRAMEWORK logic lives here so call sites can route through a single helper.
+ ///
+ public static class TestCertificates
+ {
+ ///
+ /// Returns a new to hold generated certificates on .NET Framework, or
+ /// null on other frameworks (where no certificates are generated). The returned directory, when
+ /// non-null, must be disposed by the caller.
+ ///
+ public static TmpDirectory? NewTmpDirectoryIfNeeded()
+ {
+#if NETFRAMEWORK
+ return new TmpDirectory();
+#else
+ return null;
+#endif
+ }
+
+ ///
+ /// On .NET Framework, generates a fresh unique self-signed certificate into .
+ /// On other frameworks, returns the supplied so static certificates are
+ /// shared (enabling TLS session resumption).
+ ///
+ public static CertAndThumbprint CertFor(CertAndThumbprint staticCert, TmpDirectory? tmpDirectory)
+ {
+#if NETFRAMEWORK
+ return CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory!.FullPath);
+#else
+ return staticCert;
+#endif
+ }
+ }
+}
diff --git a/source/Halibut.Tests/TestServices/Async/IAsyncClientSayHelloService.cs b/source/Halibut.Tests/TestServices/Async/IAsyncClientSayHelloService.cs
new file mode 100644
index 000000000..69c4d7b03
--- /dev/null
+++ b/source/Halibut.Tests/TestServices/Async/IAsyncClientSayHelloService.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace Halibut.Tests.TestServices.Async
+{
+ public interface IAsyncClientSayHelloService
+ {
+ Task SayHelloAsync(string name);
+ }
+}
diff --git a/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs b/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs
index b395aa909..0bf512b60 100644
--- a/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs
+++ b/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs
@@ -51,7 +51,7 @@ public async Task OctopusCanDiscoverTentacle(ClientAndServiceTestCase clientAndS
{
var info = await clientAndService.Client.DiscoverAsync(clientAndService.ServiceUri, CancellationToken);
- info.RemoteThumbprint.Should().Be(Certificates.TentacleListeningPublicThumbprint);
+ info.RemoteThumbprint.Should().Be(clientAndService.GetServiceEndPoint().RemoteThumbprint);
}
}
diff --git a/source/Halibut.Tests/Transport/SecureClientFixture.cs b/source/Halibut.Tests/Transport/SecureClientFixture.cs
index d6df4e782..124c083d7 100644
--- a/source/Halibut.Tests/Transport/SecureClientFixture.cs
+++ b/source/Halibut.Tests/Transport/SecureClientFixture.cs
@@ -27,21 +27,28 @@ public class SecureClientFixture : IAsyncDisposable
ServiceEndPoint endpoint;
HalibutRuntime tentacle;
ILog log;
+ TmpDirectory? tmpDirectory;
+ CertAndThumbprint tentacleCert;
+ CertAndThumbprint octopusCert;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
[SetUp]
public void SetUp()
{
+ tmpDirectory = TestCertificates.NewTmpDirectoryIfNeeded();
+ tentacleCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, tmpDirectory);
+ octopusCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, tmpDirectory);
+
var services = new DelegateServiceFactory();
services.Register(() => new AsyncEchoService());
tentacle = new HalibutRuntimeBuilder()
- .WithServerCertificate(Certificates.TentacleListening)
+ .WithServerCertificate(tentacleCert.Certificate2)
.WithServiceFactory(services)
.WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build())
.Build();
var tentaclePort = tentacle.Listen();
- tentacle.Trust(Certificates.OctopusPublicThumbprint);
- endpoint = new ServiceEndPoint("https://localhost:" + tentaclePort, Certificates.TentacleListeningPublicThumbprint, tentacle.TimeoutsAndLimits)
+ tentacle.Trust(octopusCert.Thumbprint);
+ endpoint = new ServiceEndPoint("https://localhost:" + tentaclePort, tentacleCert.Thumbprint, tentacle.TimeoutsAndLimits)
{
ConnectionErrorRetryTimeout = TimeSpan.MaxValue
};
@@ -51,6 +58,7 @@ public void SetUp()
public async ValueTask DisposeAsync()
{
await tentacle.DisposeAsync();
+ tmpDirectory?.Dispose();
}
[Test]
@@ -81,13 +89,12 @@ public async Task SecureClientClearsPoolWhenAllConnectionsCorrupt()
};
var tcpConnectionFactory = new TcpConnectionFactory(
- Certificates.Octopus,
+ octopusCert.Certificate2,
halibutTimeoutsAndLimits,
new StreamFactory(),
- NoOpSecureConnectionObserver.Instance,
- SslConfiguration.Default
+ NoOpSecureConnectionObserver.Instance
);
- var secureClient = new SecureListeningClient(GetProtocol, endpoint, Certificates.Octopus, log, connectionManager, tcpConnectionFactory);
+ var secureClient = new SecureListeningClient(GetProtocol, endpoint, octopusCert.Certificate2, log, connectionManager, tcpConnectionFactory);
ResponseMessage response = null!;
await secureClient.ExecuteTransactionAsync(async (mep, ct) => response = await mep.ExchangeAsClientAsync(request, ct), CancellationToken.None);
diff --git a/source/Halibut.Tests/Transport/SecureListenerFixture.cs b/source/Halibut.Tests/Transport/SecureListenerFixture.cs
index 314daf5ed..69775b77e 100644
--- a/source/Halibut.Tests/Transport/SecureListenerFixture.cs
+++ b/source/Halibut.Tests/Transport/SecureListenerFixture.cs
@@ -74,8 +74,7 @@ public async Task SecureListenerDoesNotCreateHundredsOfIoEventsPerSecondOnWindow
timeoutsAndLimits,
new StreamFactory(),
NoOpConnectionsObserver.Instance,
- NoOpSecureConnectionObserver.Instance,
- SslConfiguration.Default
+ NoOpSecureConnectionObserver.Instance
);
var idleAverage = CollectCounterValues(opsPerSec)
diff --git a/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs b/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs
index 86d1dc895..a4c60f2b5 100644
--- a/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs
+++ b/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs
@@ -1,4 +1,3 @@
-using System;
using System.Threading.Tasks;
using FluentAssertions;
using Halibut.Tests.Support;
@@ -23,8 +22,8 @@ public async Task RequestsShouldBeTakenFromAnyClient(ClientAndServiceTestCase cl
{
var clients = new[]
{
- clientOnly1.ListeningUri!,
- clientOnly2.ListeningUri!
+ (clientOnly1.ListeningUri!, clientOnly1.ClientThumbprint),
+ (clientOnly2.ListeningUri!, clientOnly2.ClientThumbprint)
};
await using (var service = await clientAndServiceTestCase.CreateServiceOnlyTestCaseBuilder()
diff --git a/source/Halibut.sln b/source/Halibut.sln
index 05e2e010b..fcb21f36a 100644
--- a/source/Halibut.sln
+++ b/source/Halibut.sln
@@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Halibut.TestUtils.CompatBinary.SchannelProbe", "Halibut.TestUtils.CompatBinary.SchannelProbe\Halibut.TestUtils.CompatBinary.SchannelProbe.csproj", "{4D26A9FA-B316-4BE3-8780-23E9136492DB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +179,18 @@ Global
{80042E24-9461-47D1-9AC5-E414E4DBF821}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{80042E24-9461-47D1-9AC5-E414E4DBF821}.Release|x86.ActiveCfg = Release|Any CPU
{80042E24-9461-47D1-9AC5-E414E4DBF821}.Release|x86.Build.0 = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Debug|x86.Build.0 = Debug|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|x86.ActiveCfg = Release|Any CPU
+ {4D26A9FA-B316-4BE3-8780-23E9136492DB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/source/Halibut/HalibutRuntime.cs b/source/Halibut/HalibutRuntime.cs
index 7233b29ec..1588239e0 100644
--- a/source/Halibut/HalibutRuntime.cs
+++ b/source/Halibut/HalibutRuntime.cs
@@ -47,7 +47,6 @@ public class HalibutRuntime : IHalibutRuntime
readonly ISecureConnectionObserver secureConnectionObserver;
readonly IActiveTcpConnectionsLimiter activeTcpConnectionsLimiter;
readonly IControlMessageObserver controlMessageObserver;
- readonly ISslConfigurationProvider sslConfigurationProvider;
internal HalibutRuntime(
IServiceFactory serviceFactory,
@@ -63,8 +62,7 @@ internal HalibutRuntime(
IRpcObserver rpcObserver,
IConnectionsObserver connectionsObserver,
IControlMessageObserver controlMessageObserver,
- ISecureConnectionObserver secureConnectionObserver,
- ISslConfigurationProvider sslConfigurationProvider
+ ISecureConnectionObserver secureConnectionObserver
)
{
this.serverCertificate = serverCertificate;
@@ -81,10 +79,9 @@ ISslConfigurationProvider sslConfigurationProvider
this.connectionsObserver = connectionsObserver;
this.secureConnectionObserver = secureConnectionObserver;
this.controlMessageObserver = controlMessageObserver;
- this.sslConfigurationProvider = sslConfigurationProvider;
connectionManager = new ConnectionManagerAsync();
- tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory, secureConnectionObserver, sslConfigurationProvider);
+ tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory, secureConnectionObserver);
activeTcpConnectionsLimiter = new ActiveTcpConnectionsLimiter(TimeoutsAndLimits);
}
@@ -139,8 +136,7 @@ public int Listen(IPEndPoint endpoint)
TimeoutsAndLimits,
streamFactory,
connectionsObserver,
- secureConnectionObserver,
- sslConfigurationProvider
+ secureConnectionObserver
);
listeners.DoWithExclusiveAccess(l =>
@@ -256,7 +252,7 @@ public async Task DiscoverAsync(Uri uri, CancellationToken canc
public async Task DiscoverAsync(ServiceEndPoint endpoint, CancellationToken cancellationToken)
{
- var client = new DiscoveryClient(streamFactory, sslConfigurationProvider);
+ var client = new DiscoveryClient(streamFactory);
return await client.DiscoverAsync(endpoint, TimeoutsAndLimits, cancellationToken);
}
diff --git a/source/Halibut/HalibutRuntimeBuilder.cs b/source/Halibut/HalibutRuntimeBuilder.cs
index acd85cc65..6bb3e7313 100644
--- a/source/Halibut/HalibutRuntimeBuilder.cs
+++ b/source/Halibut/HalibutRuntimeBuilder.cs
@@ -32,7 +32,6 @@ public class HalibutRuntimeBuilder
ISecureConnectionObserver? secureConnectionObserver;
IControlMessageObserver? controlMessageObserver;
MessageStreamWrappers queueMessageStreamWrappers = new();
- ISslConfigurationProvider? sslConfigurationProvider;
public HalibutRuntimeBuilder WithQueueMessageStreamWrappers(MessageStreamWrappers queueMessageStreamWrappers)
{
@@ -52,12 +51,6 @@ public HalibutRuntimeBuilder WithSecureConnectionObserver(ISecureConnectionObser
return this;
}
- public HalibutRuntimeBuilder WithSslConfigurationProvider(ISslConfigurationProvider sslConfigurationProvider)
- {
- this.sslConfigurationProvider = sslConfigurationProvider;
- return this;
- }
-
internal HalibutRuntimeBuilder WithStreamFactory(IStreamFactory streamFactory)
{
this.streamFactory = streamFactory;
@@ -193,7 +186,6 @@ public HalibutRuntime Build()
var secureConnectionObserver = this.secureConnectionObserver ?? NoOpSecureConnectionObserver.Instance;
var rpcObserver = this.rpcObserver ?? new NoRpcObserver();
var controlMessageObserver = this.controlMessageObserver ?? new NoOpControlMessageObserver();
- var sslConfigurationProvider = this.sslConfigurationProvider ?? SslConfiguration.Default;
var halibutRuntime = new HalibutRuntime(
serviceFactory,
@@ -209,8 +201,7 @@ public HalibutRuntime Build()
rpcObserver,
connectionsObserver,
controlMessageObserver,
- secureConnectionObserver,
- sslConfigurationProvider
+ secureConnectionObserver
);
if (onUnauthorizedClientConnect is not null)
diff --git a/source/Halibut/Transport/DefaultSslConfigurationProvider.cs b/source/Halibut/Transport/DefaultSslConfigurationProvider.cs
deleted file mode 100644
index ad354eddc..000000000
--- a/source/Halibut/Transport/DefaultSslConfigurationProvider.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2012-2013 Octopus Deploy Pty. Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Security.Authentication;
-
-namespace Halibut.Transport
-{
- ///
- /// Provides a default implementation of ISslConfigurationProvider that uses the system defaults.
- ///
- public class DefaultSslConfigurationProvider : ISslConfigurationProvider
- {
- public SslProtocols SupportedProtocols => SslProtocols.None;
- }
-}
\ No newline at end of file
diff --git a/source/Halibut/Transport/DiscoveryClient.cs b/source/Halibut/Transport/DiscoveryClient.cs
index a631c32ec..c92280702 100644
--- a/source/Halibut/Transport/DiscoveryClient.cs
+++ b/source/Halibut/Transport/DiscoveryClient.cs
@@ -16,17 +16,10 @@ public class DiscoveryClient
readonly LogFactory logs = new ();
readonly IStreamFactory streamFactory;
- readonly ISslConfigurationProvider sslConfigurationProvider;
public DiscoveryClient(IStreamFactory streamFactory)
- : this(streamFactory, SslConfiguration.Default)
- {
- }
-
- public DiscoveryClient(IStreamFactory streamFactory, ISslConfigurationProvider sslConfigurationProvider)
{
this.streamFactory = streamFactory;
- this.sslConfigurationProvider = sslConfigurationProvider;
}
public async Task DiscoverAsync(ServiceEndPoint serviceEndpoint, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, CancellationToken cancellationToken)
@@ -52,13 +45,12 @@ public async Task DiscoverAsync(ServiceEndPoint serviceEndpoint
await ssl.AuthenticateAsClientAsync(
serviceEndpoint.BaseUri.Host,
new X509Certificate2Collection(),
- sslConfigurationProvider.SupportedProtocols,
+ SslConfiguration.SupportedProtocols,
false);
#else
await ssl.AuthenticateAsClientEnforcingTimeout(
serviceEndpoint,
new X509Certificate2Collection(),
- sslConfigurationProvider,
cancellationToken
);
#endif
diff --git a/source/Halibut/Transport/ISslConfigurationProvider.cs b/source/Halibut/Transport/ISslConfigurationProvider.cs
deleted file mode 100644
index 952b45e51..000000000
--- a/source/Halibut/Transport/ISslConfigurationProvider.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2012-2013 Octopus Deploy Pty. Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System.Security.Authentication;
-
-namespace Halibut.Transport
-{
- public interface ISslConfigurationProvider
- {
- public SslProtocols SupportedProtocols { get; }
- }
-}
\ No newline at end of file
diff --git a/source/Halibut/Transport/LegacySslConfigurationProvider.cs b/source/Halibut/Transport/LegacySslConfigurationProvider.cs
deleted file mode 100644
index 67c92fa90..000000000
--- a/source/Halibut/Transport/LegacySslConfigurationProvider.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2012-2013 Octopus Deploy Pty. Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Security.Authentication;
-
-namespace Halibut.Transport
-{
- ///
- /// An implementation of ISslConfigurationProvider that uses legacy TLS protocols (1.0 and 1.1)
- /// in addition to modern ones. Protocols are explicitly specified rather than using system
- /// defaults.
- ///
- public class LegacySslConfigurationProvider : ISslConfigurationProvider
- {
-#pragma warning disable SYSLIB0039
- // See https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0039
- // TLS 1.0 and 1.1 are obsolete from .NET 7
- public SslProtocols SupportedProtocols => SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13;
-#pragma warning restore SYSLIB0039
- }
-}
\ No newline at end of file
diff --git a/source/Halibut/Transport/SecureListener.cs b/source/Halibut/Transport/SecureListener.cs
index 6a48bde2a..5132e1a4e 100644
--- a/source/Halibut/Transport/SecureListener.cs
+++ b/source/Halibut/Transport/SecureListener.cs
@@ -49,7 +49,6 @@ public class SecureListener : IAsyncDisposable
readonly IStreamFactory streamFactory;
readonly IConnectionsObserver connectionsObserver;
readonly ISecureConnectionObserver secureConnectionObserver;
- readonly ISslConfigurationProvider sslConfigurationProvider;
ILog log;
TcpListener listener;
Thread? backgroundThread;
@@ -70,8 +69,7 @@ public SecureListener(
HalibutTimeoutsAndLimits halibutTimeoutsAndLimits,
IStreamFactory streamFactory,
IConnectionsObserver connectionsObserver,
- ISecureConnectionObserver secureConnectionObserver,
- ISslConfigurationProvider sslConfigurationProvider
+ ISecureConnectionObserver secureConnectionObserver
)
{
this.endPoint = endPoint;
@@ -87,7 +85,6 @@ ISslConfigurationProvider sslConfigurationProvider
this.streamFactory = streamFactory;
this.connectionsObserver = connectionsObserver;
this.secureConnectionObserver = secureConnectionObserver;
- this.sslConfigurationProvider = sslConfigurationProvider;
this.cts = new CancellationTokenSource();
this.cancellationToken = cts.Token;
@@ -312,7 +309,7 @@ await ssl
.AuthenticateAsServerAsync(
serverCertificate,
true,
- sslConfigurationProvider.SupportedProtocols,
+ SslConfiguration.SupportedProtocols,
false)
.ConfigureAwait(false);
diff --git a/source/Halibut/Transport/SslConfiguration.cs b/source/Halibut/Transport/SslConfiguration.cs
index 254a7c41c..0626651bd 100644
--- a/source/Halibut/Transport/SslConfiguration.cs
+++ b/source/Halibut/Transport/SslConfiguration.cs
@@ -1,12 +1,9 @@
+using System.Security.Authentication;
+
namespace Halibut.Transport
{
public static class SslConfiguration
{
- public static ISslConfigurationProvider Default { get; }
-#if NETFRAMEWORK // .NET4.8 exhibited inconsistent behavior when using the default configuration
- = new LegacySslConfigurationProvider();
-#else
- = new DefaultSslConfigurationProvider();
-#endif
+ public static SslProtocols SupportedProtocols => SslProtocols.None; // None means system defaults
}
}
\ No newline at end of file
diff --git a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs
index aceddd438..857899c09 100644
--- a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs
+++ b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs
@@ -13,7 +13,6 @@ internal static async Task AuthenticateAsClientEnforcingTimeout(
this SslStream ssl,
ServiceEndPoint serviceEndpoint,
X509Certificate2Collection clientCertificates,
- ISslConfigurationProvider sslConfigurationProvider,
CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(ssl.ReadTimeout);
@@ -23,7 +22,7 @@ internal static async Task AuthenticateAsClientEnforcingTimeout(
{
TargetHost = serviceEndpoint.BaseUri.Host,
ClientCertificates = clientCertificates,
- EnabledSslProtocols = sslConfigurationProvider.SupportedProtocols,
+ EnabledSslProtocols = SslConfiguration.SupportedProtocols,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
};
diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs
index b61e190e5..10750ee97 100644
--- a/source/Halibut/Transport/TcpConnectionFactory.cs
+++ b/source/Halibut/Transport/TcpConnectionFactory.cs
@@ -22,21 +22,18 @@ public class TcpConnectionFactory : IConnectionFactory
readonly HalibutTimeoutsAndLimits halibutTimeoutsAndLimits;
readonly IStreamFactory streamFactory;
readonly ISecureConnectionObserver secureConnectionObserver;
- readonly ISslConfigurationProvider sslConfigurationProvider;
public TcpConnectionFactory(
X509Certificate2 clientCertificate,
HalibutTimeoutsAndLimits halibutTimeoutsAndLimits,
IStreamFactory streamFactory,
- ISecureConnectionObserver secureConnectionObserver,
- ISslConfigurationProvider sslConfigurationProvider
+ ISecureConnectionObserver secureConnectionObserver
)
{
this.clientCertificate = clientCertificate;
this.halibutTimeoutsAndLimits = halibutTimeoutsAndLimits;
this.streamFactory = streamFactory;
this.secureConnectionObserver = secureConnectionObserver;
- this.sslConfigurationProvider = sslConfigurationProvider;
}
public async Task EstablishNewConnectionAsync(ExchangeProtocolBuilder exchangeProtocolBuilder, ServiceEndPoint serviceEndpoint, ILog log, CancellationToken cancellationToken)
@@ -61,10 +58,10 @@ public async Task EstablishNewConnectionAsync(ExchangeProtocolBuild
await ssl.AuthenticateAsClientAsync(
serviceEndpoint.BaseUri.Host,
new X509Certificate2Collection(clientCertificate),
- sslConfigurationProvider.SupportedProtocols,
+ SslConfiguration.SupportedProtocols,
false);
#else
- await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(clientCertificate), sslConfigurationProvider, cancellationToken);
+ await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(clientCertificate), cancellationToken);
#endif
await ssl.WriteAsync(MxLine, 0, MxLine.Length, cancellationToken);