diff --git a/src/Cassandra.Tests/Cassandra.Tests.csproj b/src/Cassandra.Tests/Cassandra.Tests.csproj index cc260a25e..3bd6869a6 100644 --- a/src/Cassandra.Tests/Cassandra.Tests.csproj +++ b/src/Cassandra.Tests/Cassandra.Tests.csproj @@ -11,15 +11,23 @@ true Cassandra.Tests true - 7.1 + latest - + $(DefineConstants);NETCOREAPP + $(NoWarn);SYSLIB0051;SYSLIB0012;SYSLIB0039;CA1416 - + TargetFramework=netstandard2.0 + + + + TargetFramework=net8.0 + + + TargetFramework=netstandard2.0 diff --git a/src/Cassandra.Tests/Connections/TlsSessionTicketCacheTests.cs b/src/Cassandra.Tests/Connections/TlsSessionTicketCacheTests.cs new file mode 100644 index 000000000..9823db906 --- /dev/null +++ b/src/Cassandra.Tests/Connections/TlsSessionTicketCacheTests.cs @@ -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 + } +} diff --git a/src/Cassandra.Tests/EndianBitConverterTests.cs b/src/Cassandra.Tests/EndianBitConverterTests.cs index b6d03f5c1..93d3e4458 100644 --- a/src/Cassandra.Tests/EndianBitConverterTests.cs +++ b/src/Cassandra.Tests/EndianBitConverterTests.cs @@ -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); } } @@ -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); } } diff --git a/src/Cassandra.Tests/IOUnitTests.cs b/src/Cassandra.Tests/IOUnitTests.cs index 26c2227d9..5cf5be12c 100644 --- a/src/Cassandra.Tests/IOUnitTests.cs +++ b/src/Cassandra.Tests/IOUnitTests.cs @@ -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); diff --git a/src/Cassandra.Tests/SSLOptionsTests.cs b/src/Cassandra.Tests/SSLOptionsTests.cs new file mode 100644 index 000000000..1403835df --- /dev/null +++ b/src/Cassandra.Tests/SSLOptionsTests.cs @@ -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)); + } + } +} diff --git a/src/Cassandra.Tests/TargetTests.cs b/src/Cassandra.Tests/TargetTests.cs index 12f899125..51be2ed85 100644 --- a/src/Cassandra.Tests/TargetTests.cs +++ b/src/Cassandra.Tests/TargetTests.cs @@ -33,7 +33,11 @@ public void Should_TargetNetstandard15_When_TestsTargetNetcore20() .GetCustomAttribute()? .FrameworkName; +#if NET8_0_OR_GREATER + Assert.AreEqual(".NETCoreApp,Version=v8.0", framework); +#else Assert.AreEqual(".NETStandard,Version=v2.0", framework); +#endif } #else [Test] diff --git a/src/Cassandra.Tests/TokenTests.cs b/src/Cassandra.Tests/TokenTests.cs index 94caf4a17..647dfb267 100644 --- a/src/Cassandra.Tests/TokenTests.cs +++ b/src/Cassandra.Tests/TokenTests.cs @@ -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)) { diff --git a/src/Cassandra/Cassandra.csproj b/src/Cassandra/Cassandra.csproj index d6a54c203..dc4d83efa 100644 --- a/src/Cassandra/Cassandra.csproj +++ b/src/Cassandra/Cassandra.csproj @@ -7,7 +7,7 @@ 3.22.0.2 false DataStax and ScyllaDB - netstandard2.0 + netstandard2.0;net8.0 $(NoWarn);1591 true NU1901;NU1902;NU1903;NU1904 @@ -23,11 +23,13 @@ LICENSE.md https://github.com/scylladb/csharp-driver https://github.com/scylladb/csharp-driver - 7.1 + latest false - + $(DefineConstants);NETCOREAPP + + $(NoWarn);SYSLIB0051;SYSLIB0012;SYSLIB0039;CA1416 diff --git a/src/Cassandra/Configuration.cs b/src/Cassandra/Configuration.cs index 29d42bd7b..c3aad0c9c 100644 --- a/src/Cassandra/Configuration.cs +++ b/src/Cassandra/Configuration.cs @@ -226,6 +226,12 @@ public class Configuration internal IServerNameResolver ServerNameResolver { get; } + /// + /// Per-cluster TLS session ticket cache used for TLS session resumption. + /// Null when SSL is not configured. + /// + internal TlsSessionTicketCache TlsSessionTicketCache { get; } + internal Configuration() : this(Policies.DefaultPolicies, new ProtocolOptions(), @@ -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(); + } } /// diff --git a/src/Cassandra/Connections/Connection.cs b/src/Cassandra/Connections/Connection.cs index 0387dfd74..1e5926a92 100644 --- a/src/Cassandra/Connections/Connection.cs +++ b/src/Cassandra/Connections/Connection.cs @@ -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 diff --git a/src/Cassandra/Connections/TcpSocket.cs b/src/Cassandra/Connections/TcpSocket.cs index 554fa877a..9d7dd751b 100644 --- a/src/Cassandra/Connections/TcpSocket.cs +++ b/src/Cassandra/Connections/TcpSocket.cs @@ -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 { @@ -52,6 +55,12 @@ internal class TcpSocket : ITcpSocket public SSLOptions SSLOptions { get; set; } + /// + /// The per-cluster TLS session ticket cache for session resumption. + /// May be null if SSL is not configured. + /// + internal TlsSessionTicketCache TlsSessionTicketCache { get; set; } + /// /// Event that gets fired when new data is received. /// @@ -72,11 +81,12 @@ internal class TcpSocket : ITcpSocket /// /// Creates a new instance of TcpSocket using the endpoint and options provided. /// - 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) { @@ -199,6 +209,36 @@ private async Task 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( + 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 @@ -226,6 +266,7 @@ private async Task ConnectSsl() await tcs.Task.ConfigureAwait(false); TcpSocket.Logger.Verbose("SSL authentication successful"); +#endif ReceiveAsync(); return true; } diff --git a/src/Cassandra/Connections/TlsSessionTicketCache.cs b/src/Cassandra/Connections/TlsSessionTicketCache.cs new file mode 100644 index 000000000..83a627ff3 --- /dev/null +++ b/src/Cassandra/Connections/TlsSessionTicketCache.cs @@ -0,0 +1,78 @@ +// +// 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. +// + +#if NET8_0_OR_GREATER +using System.Collections.Concurrent; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +#endif + +namespace Cassandra.Connections +{ + /// + /// Provides TLS session ticket caching for a Cluster instance. + /// On .NET 8.0+, this uses SslClientAuthenticationOptions with + /// AllowTlsResume = true to enable TLS session resumption, + /// reducing handshake overhead for subsequent connections to the same host. + /// The same SslClientAuthenticationOptions instance is reused for all + /// connections to a given server name, which is required by .NET for session + /// ticket reuse. + /// On older runtimes (netstandard2.0), this class is a no-op placeholder. + /// + internal class TlsSessionTicketCache + { + private static readonly Logger Logger = new Logger(typeof(TlsSessionTicketCache)); + +#if NET8_0_OR_GREATER + private readonly ConcurrentDictionary _cache = + new ConcurrentDictionary(); + + /// + /// Returns a cached for the given server name, + /// creating one on first access. The same instance is returned for subsequent calls with + /// the same , which is required by .NET for TLS session resumption. + /// + /// The target host name for TLS SNI and certificate validation. + /// The SSL options configured on the cluster. + /// A cached instance. + public SslClientAuthenticationOptions GetAuthenticationOptions(string serverName, SSLOptions sslOptions) + { + return _cache.GetOrAdd(serverName, key => + { + var options = new SslClientAuthenticationOptions + { + TargetHost = key, + ClientCertificates = sslOptions.CertificateCollection, + EnabledSslProtocols = sslOptions.SslProtocol, + CertificateRevocationCheckMode = sslOptions.CheckCertificateRevocation + ? X509RevocationMode.Online + : X509RevocationMode.NoCheck, + RemoteCertificateValidationCallback = sslOptions.RemoteCertValidationCallback, + AllowTlsResume = sslOptions.EnableSessionResumption + }; + + Logger.Verbose( + "Created SslClientAuthenticationOptions for host '{0}' (AllowTlsResume={1})", + key, + options.AllowTlsResume); + + return options; + }); + } +#endif + } +} diff --git a/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs b/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs index a6e23b6b4..44af61644 100644 --- a/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs +++ b/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs @@ -41,6 +41,39 @@ internal class CqlExpressionVisitor : ExpressionVisitor private static readonly string Utf8MaxValue = Encoding.UTF8.GetString(new byte[] { 0xF4, 0x8F, 0xBF, 0xBF }); + /// + /// Evaluates an expression by compiling it into a typed Func<T> delegate + /// and invoking it directly. This avoids: + /// 1. DynamicInvoke - which throws NotSupportedException on .NET 8+ for compiled delegates + /// 2. Expression.Convert to object - which can throw InvalidProgramException on .NET 9+ + /// + private static object EvaluateExpressionValue(Expression expression) + { + // Ref structs (e.g. ReadOnlySpan, Span) cannot be used as generic type arguments. + // In .NET 9+, Enumerable.Contains() has overloads that accept ReadOnlySpan, so the + // collection argument in the expression tree may be wrapped in a Convert node to + // ReadOnlySpan. Unwrap it to recover the underlying enumerable. + if (expression.Type.IsByRefLike) + { + if (expression is UnaryExpression unary && unary.NodeType == ExpressionType.Convert) + { + return EvaluateExpressionValue(unary.Operand); + } + throw new InvalidOperationException( + $"Cannot evaluate expression of by-ref-like type '{expression.Type}'. " + + "This may be caused by a ReadOnlySpan overload being selected (e.g. Enumerable.Contains in .NET 9+)."); + } + return EvalMethod.MakeGenericMethod(expression.Type).Invoke(null, new object[] { expression }); + } + + private static readonly MethodInfo EvalMethod = + typeof(CqlExpressionVisitor).GetMethod(nameof(CompileAndInvoke), BindingFlags.NonPublic | BindingFlags.Static); + + private static object CompileAndInvoke(Expression expression) + { + return Expression.Lambda>(expression).Compile().Invoke(); + } + private static readonly HashSet CqlUnsupTags = new HashSet { ExpressionType.Or, @@ -577,7 +610,7 @@ private Expression AddProjection(Expression node, PocoColumn column = null) } else { - value = Expression.Lambda(node).Compile().DynamicInvoke(); + value = EvaluateExpressionValue(node); } if (column == null) { @@ -607,7 +640,7 @@ private Expression EvaluateConditionFunction(MethodCallExpression node) case nameof(string.StartsWith) when node.Method.DeclaringType == typeof(string): Visit(node.Object); var startsWithArgument = node.Arguments[0]; - var startString = (string)Expression.Lambda(startsWithArgument).Compile().DynamicInvoke(); + var startString = (string)EvaluateExpressionValue(startsWithArgument); var endString = startString + CqlExpressionVisitor.Utf8MaxValue; // Create 2 conditions, ie: WHERE col1 >= startString AND col2 < endString var column = condition.Column; @@ -650,7 +683,7 @@ private Expression EvaluateConditionFunction(MethodCallExpression node) return node; } // Try to invoke to obtain the parameter value - condition.SetParameter(Expression.Lambda(node).Compile().DynamicInvoke()); + condition.SetParameter(EvaluateExpressionValue(node)); return node; } @@ -678,7 +711,7 @@ private void EvaluateContainsMethod(MethodCallExpression node) EvaluateCompositeColumn(columnExpression); } - var values = Expression.Lambda(parameterExpression).Compile().DynamicInvoke() as IEnumerable; + var values = EvaluateExpressionValue(parameterExpression) as IEnumerable; if (values == null) { throw new InvalidOperationException("Contains parameter must be IEnumerable"); @@ -751,7 +784,7 @@ private bool EvaluateOperatorMethod(MethodCallExpression node) } // Use the last argument (valid for maps and list/sets) var argument = node.Arguments[node.Arguments.Count - 1]; - var value = Expression.Lambda(argument).Compile().DynamicInvoke(); + var value = EvaluateExpressionValue(argument); _projections.Add(Tuple.Create(column, value, expressionType)); return true; } @@ -788,7 +821,7 @@ protected override Expression VisitUnary(UnaryExpression node) } else { - var val = Expression.Lambda(node).Compile().DynamicInvoke(); + var val = EvaluateExpressionValue(node); condition.SetParameter(val); } return node; @@ -803,7 +836,7 @@ protected override Expression VisitUnary(UnaryExpression node) } if (column != null && column.IsCounter) { - var value = Expression.Lambda(node).Compile().DynamicInvoke(); + var value = EvaluateExpressionValue(node); if (!(value is long || value is int)) { throw new ArgumentException("Only Int64 and Int32 values are supported as counter increment of decrement values"); @@ -876,7 +909,7 @@ protected override Expression VisitBinary(BinaryExpression node) if (!CqlExpressionVisitor.CqlUnsupTags.Contains(node.NodeType)) { - condition.SetParameter(Expression.Lambda(node).Compile().DynamicInvoke()); + condition.SetParameter(EvaluateExpressionValue(node)); return node; } } @@ -1115,7 +1148,7 @@ private static object GetClosureValue(MemberExpression node) } else { - value = Expression.Lambda(node).Compile().DynamicInvoke(); + value = EvaluateExpressionValue(node); } return value; } diff --git a/src/Cassandra/Helpers/PlatformHelper.cs b/src/Cassandra/Helpers/PlatformHelper.cs index 01084c1da..3f0d77ddd 100644 --- a/src/Cassandra/Helpers/PlatformHelper.cs +++ b/src/Cassandra/Helpers/PlatformHelper.cs @@ -33,7 +33,9 @@ public static bool IsKerberosSupported() public static string GetTargetFramework() { -#if NETSTANDARD2_0 +#if NET8_0_OR_GREATER + return ".NET 8.0+"; +#elif NETSTANDARD2_0 return ".NET Standard 2.0"; #else return null; diff --git a/src/Cassandra/SSLOptions.cs b/src/Cassandra/SSLOptions.cs index b2aef4ea8..f4d49c735 100644 --- a/src/Cassandra/SSLOptions.cs +++ b/src/Cassandra/SSLOptions.cs @@ -33,6 +33,7 @@ public class SSLOptions private bool _checkCertificateRevocation; private X509CertificateCollection _certificateCollection = new X509CertificateCollection(); private Func _hostNameResolver = GetHostName; + private bool _enableSessionResumption = true; /// /// Verifies Cassandra host SSL certificate used for authentication. @@ -75,21 +76,32 @@ public X509CertificateCollection CertificateCollection } /// - /// Creates SSLOptions with default values. + /// Gets whether TLS session resumption (session tickets) is enabled. + /// When enabled, subsequent TLS connections to the same host can reuse + /// previously negotiated session parameters, reducing handshake overhead. + /// Effective on .NET 8.0 and later. Defaults to true. + /// + public bool EnableSessionResumption + { + get { return _enableSessionResumption; } + } + + /// + /// Creates SSLOptions with default values. /// public SSLOptions() { } /// - /// Creates SSL options used for SSL connections with Casandra hosts. + /// Creates SSL options used for SSL connections with Casandra hosts. /// /// type of SSL protocol, default set to Tls. /// specifies whether the certificate revocation list is checked during connection authentication. /// verifies Cassandra host SSL certificate used for authentication. /// - /// Default RemoteCertificateValidationCallback won't establish a connection if any error will occur. - /// + /// Default RemoteCertificateValidationCallback won't establish a connection if any error will occur. + /// /// public SSLOptions(SslProtocols sslProtocol, bool checkCertificateRevocation, RemoteCertificateValidationCallback remoteCertValidationCallback) { @@ -134,6 +146,18 @@ public SSLOptions SetRemoteCertValidationCallback(RemoteCertificateValidationCal return this; } + /// + /// Sets whether TLS session resumption (session tickets) is enabled. + /// When enabled, subsequent TLS connections to the same host can reuse + /// previously negotiated session parameters, reducing handshake overhead. + /// Effective on .NET 8.0 and later. Defaults to true. + /// + public SSLOptions SetEnableSessionResumption(bool flag) + { + _enableSessionResumption = flag; + return this; + } + private static bool ValidateServerCertificate( object sender, X509Certificate certificate,