Skip to content
Draft
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
14 changes: 11 additions & 3 deletions src/Cassandra.Tests/Cassandra.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@
<SignAssembly>true</SignAssembly>
<PackageId>Cassandra.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<LangVersion>7.1</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d$'))">
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d+$'))">
<DefineConstants>$(DefineConstants);NETCOREAPP</DefineConstants>
<NoWarn>$(NoWarn);SYSLIB0051;SYSLIB0012;SYSLIB0039;CA1416</NoWarn>
</PropertyGroup>
<ItemGroup>
<ItemGroup Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net[89]$|^net\d{2,}$'))">
<ProjectReference Include="..\Cassandra\Cassandra.csproj">
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
</ItemGroup>
<ItemGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net[89]$|^net\d{2,}$'))">
<ProjectReference Include="..\Cassandra\Cassandra.csproj">
<SetTargetFramework>TargetFramework=net8.0</SetTargetFramework>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Extensions\Cassandra.AppMetrics\Cassandra.AppMetrics.csproj">
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
Expand Down
97 changes: 97 additions & 0 deletions src/Cassandra.Tests/Connections/TlsSessionTicketCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// Copyright (C) DataStax Inc.
//
// 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;
using Cassandra.Connections;
using NUnit.Framework;

namespace Cassandra.Tests.Connections
{
[TestFixture]
public class TlsSessionTicketCacheTests
{
#if NET8_0_OR_GREATER
[Test]
public void GetAuthenticationOptions_Should_SetAllowTlsResume_True_By_Default()
{
var cache = new TlsSessionTicketCache();
var sslOptions = new SSLOptions();
var authOpts = cache.GetAuthenticationOptions("myhost.example.com", sslOptions);

Assert.That(authOpts.AllowTlsResume, Is.True);
Assert.That(authOpts.TargetHost, Is.EqualTo("myhost.example.com"));
Assert.That(authOpts.EnabledSslProtocols, Is.EqualTo(SslProtocols.Tls));
Assert.That(authOpts.CertificateRevocationCheckMode,
Is.EqualTo(System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck));
}

[Test]
public void GetAuthenticationOptions_Should_SetAllowTlsResume_False_When_Disabled()
{
var cache = new TlsSessionTicketCache();
var sslOptions = new SSLOptions().SetEnableSessionResumption(false);
var authOpts = cache.GetAuthenticationOptions("myhost.example.com", sslOptions);

Assert.That(authOpts.AllowTlsResume, Is.False);
}

[Test]
public void GetAuthenticationOptions_Should_Set_RevocationMode_Online_When_CheckRevocation_True()
{
var cache = new TlsSessionTicketCache();
var sslOptions = new SSLOptions().SetCertificateRevocationCheck(true);
var authOpts = cache.GetAuthenticationOptions("myhost.example.com", sslOptions);

Assert.That(authOpts.CertificateRevocationCheckMode,
Is.EqualTo(System.Security.Cryptography.X509Certificates.X509RevocationMode.Online));
}

[Test]
public void GetAuthenticationOptions_Should_PreserveRemoteCertValidationCallback()
{
var cache = new TlsSessionTicketCache();
System.Net.Security.RemoteCertificateValidationCallback customCallback =
(sender, cert, chain, errors) => true;
var sslOptions = new SSLOptions().SetRemoteCertValidationCallback(customCallback);
var authOpts = cache.GetAuthenticationOptions("myhost.example.com", sslOptions);

Assert.That(authOpts.RemoteCertificateValidationCallback, Is.SameAs(customCallback));
}

[Test]
public void GetAuthenticationOptions_Should_Return_Same_Instance_For_Same_Host()
{
var cache = new TlsSessionTicketCache();
var sslOptions = new SSLOptions();
var opts1 = cache.GetAuthenticationOptions("host1.example.com", sslOptions);
var opts2 = cache.GetAuthenticationOptions("host1.example.com", sslOptions);

Assert.That(opts1, Is.SameAs(opts2));
}

[Test]
public void GetAuthenticationOptions_Should_Return_Different_Instances_For_Different_Hosts()
{
var cache = new TlsSessionTicketCache();
var sslOptions = new SSLOptions();
var opts1 = cache.GetAuthenticationOptions("host1.example.com", sslOptions);
var opts2 = cache.GetAuthenticationOptions("host2.example.com", sslOptions);

Assert.That(opts1, Is.Not.SameAs(opts2));
}
#endif
}
}
8 changes: 4 additions & 4 deletions src/Cassandra.Tests/EndianBitConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public void ToInt32_SetBytes_Test()
EndianBitConverter.SetBytes(false, buffer, 0, v.Item1);
CollectionAssert.AreEqual(v.Item2, buffer);
EndianBitConverter.SetBytes(true, buffer, 0, v.Item1);
CollectionAssert.AreEqual(v.Item2.Reverse(), buffer);
CollectionAssert.AreEqual(v.Item2.AsEnumerable().Reverse(), buffer);
var decoded = EndianBitConverter.ToInt32(false, v.Item2, 0);
Assert.AreEqual(v.Item1, decoded);
decoded = EndianBitConverter.ToInt32(true, v.Item2.Reverse().ToArray(), 0);
decoded = EndianBitConverter.ToInt32(true, v.Item2.AsEnumerable().Reverse().ToArray(), 0);
Assert.AreEqual(v.Item1, decoded);
}
}
Expand All @@ -65,10 +65,10 @@ public void ToDouble_SetBytes_Test()
EndianBitConverter.SetBytes(false, buffer, 0, v.Item1);
CollectionAssert.AreEqual(v.Item2, buffer);
EndianBitConverter.SetBytes(true, buffer, 0, v.Item1);
CollectionAssert.AreEqual(v.Item2.Reverse(), buffer);
CollectionAssert.AreEqual(v.Item2.AsEnumerable().Reverse(), buffer);
var decoded = EndianBitConverter.ToDouble(false, v.Item2, 0);
Assert.AreEqual(v.Item1, decoded);
decoded = EndianBitConverter.ToDouble(true, v.Item2.Reverse().ToArray(), 0);
decoded = EndianBitConverter.ToDouble(true, v.Item2.AsEnumerable().Reverse().ToArray(), 0);
Assert.AreEqual(v.Item1, decoded);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Cassandra.Tests/IOUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void OperationState_Can_Concurrently_Get_Timeout_And_Response()
if ((counter++) % 2 == 0)
{
//invert order
actions = actions.Reverse().ToArray();
actions = actions.AsEnumerable().Reverse().ToArray();
}
TestHelper.ParallelInvoke(actions);
}, times);
Expand Down
46 changes: 46 additions & 0 deletions src/Cassandra.Tests/SSLOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright (C) DataStax Inc.
//
// 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 NUnit.Framework;

namespace Cassandra.Tests
{
[TestFixture]
public class SSLOptionsTests
{
[Test]
public void EnableSessionResumption_Should_Default_To_True()
{
var options = new SSLOptions();
Assert.That(options.EnableSessionResumption, Is.True);
}

[Test]
public void SetEnableSessionResumption_Should_Set_Value_False()
{
var options = new SSLOptions().SetEnableSessionResumption(false);
Assert.That(options.EnableSessionResumption, Is.False);
}

[Test]
public void SetEnableSessionResumption_Should_Return_Same_Instance()
{
var options = new SSLOptions();
var returned = options.SetEnableSessionResumption(true);
Assert.That(returned, Is.SameAs(options));
}
}
}
4 changes: 4 additions & 0 deletions src/Cassandra.Tests/TargetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ public void Should_TargetNetstandard15_When_TestsTargetNetcore20()
.GetCustomAttribute<TargetFrameworkAttribute>()?
.FrameworkName;

#if NET8_0_OR_GREATER
Assert.AreEqual(".NETCoreApp,Version=v8.0", framework);
#else
Assert.AreEqual(".NETStandard,Version=v2.0", framework);
#endif
}
#else
[Test]
Expand Down
3 changes: 1 addition & 2 deletions src/Cassandra.Tests/TokenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,7 @@ public void Should_UpdateKeyspacesAndTokenMapCorrectly_When_MultipleThreadsCalli
}
}
}
else
if (j % 2 == 0)
else if (j % 2 == 0)
{
if (bag.TryTake(out var ksName))
{
Expand Down
8 changes: 5 additions & 3 deletions src/Cassandra/Cassandra.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<FileVersion>3.22.0.2</FileVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Authors>DataStax and ScyllaDB</Authors>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<NoWarn>$(NoWarn);1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
Expand All @@ -23,11 +23,13 @@
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<RepositoryUrl>https://github.com/scylladb/csharp-driver</RepositoryUrl>
<PackageProjectUrl>https://github.com/scylladb/csharp-driver</PackageProjectUrl>
<LangVersion>7.1</LangVersion>
<LangVersion>latest</LangVersion>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d$'))">
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d+\.\d+$'))">
<DefineConstants>$(DefineConstants);NETCOREAPP</DefineConstants>
<!-- Suppress obsolete/platform warnings for pre-existing code when building for net8.0+ -->
<NoWarn>$(NoWarn);SYSLIB0051;SYSLIB0012;SYSLIB0039;CA1416</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions src/Cassandra/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ public class Configuration

internal IServerNameResolver ServerNameResolver { get; }

/// <summary>
/// Per-cluster TLS session ticket cache used for TLS session resumption.
/// Null when SSL is not configured.
/// </summary>
internal TlsSessionTicketCache TlsSessionTicketCache { get; }

internal Configuration() :
this(Policies.DefaultPolicies,
new ProtocolOptions(),
Expand Down Expand Up @@ -361,6 +367,11 @@ internal Configuration(Policies policies,
// to create the instance.
BufferPool = new RecyclableMemoryStreamManager(16 * 1024, 256 * 1024, ProtocolOptions.MaximumFrameLength);
Timer = new HashedWheelTimer();

if (ProtocolOptions.SslOptions != null)
{
TlsSessionTicketCache = new TlsSessionTicketCache();
}
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Cassandra/Connections/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ internal Connection(
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_startupRequestFactory = startupRequestFactory ?? throw new ArgumentNullException(nameof(startupRequestFactory));
_heartBeatInterval = configuration.GetHeartBeatInterval() ?? 0;
_tcpSocket = new TcpSocket(endPoint, configuration.SocketOptions, configuration.ProtocolOptions.SslOptions);
_tcpSocket = new TcpSocket(endPoint, configuration.SocketOptions, configuration.ProtocolOptions.SslOptions, configuration.TlsSessionTicketCache);
_idleTimer = new Timer(IdleTimeoutHandler, null, Timeout.Infinite, Timeout.Infinite);
_connectionObserver = connectionObserver;
_timerEnabled = configuration.MetricsEnabled
Expand Down
43 changes: 42 additions & 1 deletion src/Cassandra/Connections/TcpSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
using System.Threading.Tasks;
using Cassandra.Tasks;
using Microsoft.IO;
#if NET8_0_OR_GREATER
using System.Security.Authentication;
#endif

namespace Cassandra.Connections
{
Expand Down Expand Up @@ -52,6 +55,12 @@ internal class TcpSocket : ITcpSocket

public SSLOptions SSLOptions { get; set; }

/// <summary>
/// The per-cluster TLS session ticket cache for session resumption.
/// May be null if SSL is not configured.
/// </summary>
internal TlsSessionTicketCache TlsSessionTicketCache { get; set; }

/// <summary>
/// Event that gets fired when new data is received.
/// </summary>
Expand All @@ -72,11 +81,12 @@ internal class TcpSocket : ITcpSocket
/// <summary>
/// Creates a new instance of TcpSocket using the endpoint and options provided.
/// </summary>
public TcpSocket(IConnectionEndPoint endPoint, SocketOptions options, SSLOptions sslOptions)
public TcpSocket(IConnectionEndPoint endPoint, SocketOptions options, SSLOptions sslOptions, TlsSessionTicketCache tlsSessionTicketCache = null)
{
EndPoint = endPoint;
Options = options;
SSLOptions = sslOptions;
TlsSessionTicketCache = tlsSessionTicketCache;

_socket = new Socket(EndPoint.SocketIpEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
Expand Down Expand Up @@ -199,6 +209,36 @@ private async Task<bool> ConnectSsl()
TcpSocket.Logger.Verbose("Socket connected, starting SSL client authentication");
//Stream mode: not the most performant but it has ssl support
TcpSocket.Logger.Verbose("Starting SSL authentication");

#if NET8_0_OR_GREATER
var serverName = await EndPoint.GetServerNameAsync().ConfigureAwait(false);
var sslStream = new SslStream(new NetworkStream(_socket), false);
_socketStream = sslStream;

// Use a timer to ensure that it does callback
var tcs = TaskHelper.TaskCompletionSourceWithTimeout<bool>(
Options.ConnectTimeoutMillis,
() => new TimeoutException("The timeout period elapsed prior to completion of SSL authentication operation."));

var authOptions = TlsSessionTicketCache.GetAuthenticationOptions(serverName, SSLOptions);
TcpSocket.Logger.Verbose("Using SslClientAuthenticationOptions with AllowTlsResume={0}", authOptions.AllowTlsResume);

sslStream.AuthenticateAsClientAsync(authOptions)
.ContinueWith(t =>
{
if (t.Exception != null)
{
t.Exception.Handle(_ => true);
tcs.TrySetException(t.Exception.InnerException);
return;
}
tcs.TrySetResult(true);
}, TaskContinuationOptions.ExecuteSynchronously)
.Forget();

await tcs.Task.ConfigureAwait(false);
TcpSocket.Logger.Verbose("SSL authentication successful");
#else
var sslStream = new SslStream(new NetworkStream(_socket), false, SSLOptions.RemoteCertValidationCallback, null);
_socketStream = sslStream;
// Use a timer to ensure that it does callback
Expand Down Expand Up @@ -226,6 +266,7 @@ private async Task<bool> ConnectSsl()

await tcs.Task.ConfigureAwait(false);
TcpSocket.Logger.Verbose("SSL authentication successful");
#endif
ReceiveAsync();
return true;
}
Expand Down
Loading
Loading