Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions kx.Test/Connection/ConnectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
#if NETCOREAPP3_1_OR_GREATER
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
#endif
using System.Threading.Tasks;
using kx.Test.TestUtils;
using NUnit.Framework;
Expand Down Expand Up @@ -90,6 +96,121 @@ public void ConnectionThrowsSerialisableExpectionIfAuthenticationDoesNotPass()
}
}

[Test]
public void ConnectionInitialisesUsingIpv6Socket()
{
if (!Socket.OSSupportsIPv6)
{
Assert.Ignore("IPv6 is not supported on this host.");
}

TestableIpv6Server server;
try
{
server = new TestableIpv6Server();
}
catch (SocketException exception)
when (exception.SocketErrorCode == SocketError.AddressNotAvailable)
{
Assert.Ignore("IPv6 loopback is not configured on this host.");
return;
}

using (server)
using (var connection = new c(
IPAddress.IPv6Loopback.ToString(),
server.Port,
Environment.UserName,
1024,
false,
c.IpVersionPreference.IPv6))
{
Assert.IsNotNull(connection);
}
}

[Test]
public void ConnectionInitialisesUsingDualStackSocket()
{
using (var server = new TestableTcpServer())
using (var connection = new c(
IPAddress.Loopback.ToString(),
server.TestPort,
Environment.UserName,
1024,
false,
c.IpVersionPreference.DualStack))
{
Assert.IsNotNull(connection);
}
}

#if NETCOREAPP3_1_OR_GREATER
[Test]
public void ConnectionInitialisesUsingUnixDomainSocket()
{
using (var server = new TestableUnixDomainSocketServer())
using (var connection = new c(server.SocketPath, Environment.UserName))
{
Assert.IsNotNull(connection);
}
}

[Test]
public void UnixDomainSocketConstructorAcceptsNullTlsOptionsAsDisabled()
{
using (var server = new TestableUnixDomainSocketServer())
using (var connection = new c(server.SocketPath, Environment.UserName, 1024, null))
{
Assert.IsNotNull(connection);
}
}

[TestCase(true)]
[TestCase(false)]
public void ConnectionInitialisesUsingTls(bool useExplicitValues)
{
using (var server = new TestableTlsServer())
{
KdbTlsOptions options = KdbTls.Insecure(
useExplicitValues ? "localhost" : null);
options.EnabledSslProtocols =
useExplicitValues ? SslProtocols.Tls12 : (SslProtocols?)null;
options.CertificateRevocationCheckMode = useExplicitValues
? X509RevocationMode.NoCheck
: X509RevocationMode.Offline;

using (var connection = new c(
"localhost",
server.Port,
Environment.UserName,
1024,
options))
{
Assert.IsNotNull(connection);
}
}
}

[Test]
public void TlsBooleanOverloadRejectsUntrustedCertificate()
{
using (var server = new TestableTlsServer(allowClientRejection: true))
{
Exception exception = Assert.Catch(() => new c(
"localhost",
server.Port,
Environment.UserName,
1024,
true));

Assert.IsTrue(
exception is AuthenticationException || exception is IOException,
$"Expected a TLS authentication failure but received {exception.GetType().FullName}.");
}
}
#endif

[Test]
public void ProtectedConstructorAndBufferStateAreAccessibleToDerivedTypes()
{
Expand Down
180 changes: 180 additions & 0 deletions kx.Test/Connection/TestableConnectionServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace kx.Test.TestUtils
{
internal sealed class TestableIpv6Server : IDisposable
{
private readonly TcpListener _listener;
private readonly Task _serverTask;

internal TestableIpv6Server()
{
_listener = new TcpListener(IPAddress.IPv6Loopback, 0);
_listener.Server.DualMode = false;
try
{
_listener.Start();
}
catch
{
_listener.Stop();
throw;
}
_serverTask = Task.Run(() =>
{
using (TcpClient client = _listener.AcceptTcpClient())
{
TestableConnectionServer.PerformKdbHandshake(client.GetStream());
}
});
}

internal int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;

public void Dispose()
{
_listener.Stop();
TestableConnectionServer.WaitForServer(_serverTask);
}
}

#if NETCOREAPP3_1_OR_GREATER
internal sealed class TestableUnixDomainSocketServer : IDisposable
{
private readonly string _path;
private readonly Socket _listener;
private readonly Task _serverTask;

internal TestableUnixDomainSocketServer()
{
_path = Path.Combine(Path.GetTempPath(), $"csharpkdb-{Guid.NewGuid():N}.sock");
_listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
_listener.Bind(new UnixDomainSocketEndPoint(_path));
_listener.Listen(1);
_serverTask = Task.Run(() =>
{
using (Socket socket = _listener.Accept())
using (var stream = new NetworkStream(socket, ownsSocket: false))
{
TestableConnectionServer.PerformKdbHandshake(stream);
}
});
}

internal string SocketPath => _path;

public void Dispose()
{
_listener.Dispose();
TestableConnectionServer.WaitForServer(_serverTask);
if (File.Exists(_path))
{
File.Delete(_path);
}
}
}

internal sealed class TestableTlsServer : IDisposable
{
private readonly TcpListener _listener;
private readonly RSA _key;
private readonly X509Certificate2 _certificate;
private readonly Task _serverTask;
private volatile TcpClient _acceptedClient;

internal TestableTlsServer(bool allowClientRejection = false)
{
_key = RSA.Create(2048);
var request = new CertificateRequest(
"CN=localhost",
_key,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
var names = new SubjectAlternativeNameBuilder();
names.AddDnsName("localhost");
request.CertificateExtensions.Add(names.Build());
string password = Guid.NewGuid().ToString("N");
using (X509Certificate2 generatedCertificate = request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddMinutes(-5),
DateTimeOffset.UtcNow.AddDays(1)))
{
_certificate = new X509Certificate2(
generatedCertificate.Export(X509ContentType.Pfx, password),
password,
X509KeyStorageFlags.Exportable |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet);
}

_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
_serverTask = Task.Run(() =>
{
try
{
using (TcpClient client = _listener.AcceptTcpClient())
using (var stream = new SslStream(client.GetStream(), false))
{
_acceptedClient = client;
stream.AuthenticateAsServer(
_certificate,
false,
SslProtocols.Tls12,
false);
TestableConnectionServer.PerformKdbHandshake(stream);
}
}
catch when (allowClientRejection)
{
// Expected when default validation rejects the self-signed certificate.
}
});
}

internal int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;

public void Dispose()
{
_listener.Stop();
_acceptedClient?.Close();
TestableConnectionServer.WaitForServer(_serverTask);
_certificate.Dispose();
_key.Dispose();
}
}
#endif

internal static class TestableConnectionServer
{
internal static void PerformKdbHandshake(Stream stream)
{
var buffer = new byte[256];
if (stream.Read(buffer, 0, buffer.Length) == 0)
{
throw new IOException("The client closed before sending the KDB+ handshake.");
}
stream.WriteByte(3);
stream.Flush();
}

internal static void WaitForServer(Task serverTask)
{
if (!serverTask.Wait(TimeSpan.FromSeconds(5)))
{
throw new TimeoutException("The test server did not complete.");
}
}
}
}
Loading