This is an automated email from the ASF dual-hosted git repository. ptupitsyn pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 8cab5a5337 IGNITE-18884 .NET: Add TLS support (#1752) 8cab5a5337 is described below commit 8cab5a533721220db34626d75106fd17465806b0 Author: Pavel Tupitsyn <ptupit...@apache.org> AuthorDate: Tue Mar 7 11:15:16 2023 +0300 IGNITE-18884 .NET: Add TLS support (#1752) **Public API Changes** * Add `IgniteClientConfiguration.SslStreamFactory` - same API as in 2.x * Allow the factory to return `null` to disable SSL * Change `IIgniteClient.GetConnections` to return a list of `IConnectionInfo` (`IClusterNode` + `ISslInfo`) **Other Changes** * Fix `ClientHandlerModule` to initialize `SslContext` once on startup: * Improves performance (no need to initialize on every client connection) * Fails node startup in case of SSL misconfiguration * Use 4 nodes in `PlatformTestNodeRunner`: 2 without SSL, 1 with SSL, 1 with SSL and client authentication. --- .../client/handler/ItSslClientHandlerTest.java | 18 +- .../ignite/client/handler/ClientHandlerModule.java | 7 +- .../Apache.Ignite.Tests/Apache.Ignite.Tests.csproj | 9 + .../Apache.Ignite.Tests/ClientSocketTests.cs | 11 +- .../Compute/ComputeClusterAwarenessTests.cs | 2 +- .../Apache.Ignite.Tests/Compute/ComputeTests.cs | 21 ++- .../dotnet/Apache.Ignite.Tests/JavaServer.cs | 7 + .../dotnet/Apache.Ignite.Tests/MultiClusterTest.cs | 2 +- .../dotnet/Apache.Ignite.Tests/SslTests.cs | 189 +++++++++++++++++++++ .../dotnet/Apache.Ignite.Tests/keystore.pfx | Bin 0 -> 4670 bytes .../dotnet/Apache.Ignite.Tests/truststore.pfx | Bin 0 -> 1814 bytes .../dotnet/Apache.Ignite/IIgniteClient.cs | 2 +- .../ConnectionContext.cs => ISslStreamFactory.cs} | 28 +-- .../Apache.Ignite/IgniteClientConfiguration.cs | 10 ++ .../Apache.Ignite/Internal/ClientFailoverSocket.cs | 10 +- .../dotnet/Apache.Ignite/Internal/ClientSocket.cs | 73 ++++++-- .../Apache.Ignite/Internal/ConnectionContext.cs | 8 +- .../Apache.Ignite/Internal/IgniteClientInternal.cs | 4 +- .../ConnectionInfo.cs} | 17 +- .../{ConnectionContext.cs => Network/SslInfo.cs} | 25 ++- .../IConnectionInfo.cs} | 21 ++- .../dotnet/Apache.Ignite/Network/ISslInfo.cs | 58 +++++++ .../dotnet/Apache.Ignite/SslStreamFactory.cs | 105 ++++++++++++ modules/platforms/dotnet/DEVNOTES.md | 2 + .../runner/app/PlatformTestNodeRunner.java | 73 +++++++- .../src/integrationTest/resources/ssl/client.pfx | Bin 0 -> 2717 bytes .../src/integrationTest/resources/ssl/server.jks | Bin 0 -> 2392 bytes .../src/integrationTest/resources/ssl/trust.jks | Bin 0 -> 1199 bytes 28 files changed, 609 insertions(+), 93 deletions(-) diff --git a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItSslClientHandlerTest.java b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItSslClientHandlerTest.java index 585167418d..b341350c36 100644 --- a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItSslClientHandlerTest.java +++ b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItSslClientHandlerTest.java @@ -17,6 +17,7 @@ package org.apache.ignite.client.handler; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -34,6 +35,7 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import org.apache.ignite.internal.testframework.WorkDirectory; import org.apache.ignite.internal.testframework.WorkDirectoryExtension; +import org.apache.ignite.lang.IgniteException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -68,7 +70,7 @@ public class ItSslClientHandlerTest { throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(null, null); - ks.setKeyEntry("key", cert.key(), null, new Certificate[]{cert.cert()}); + ks.setKeyEntry("key", cert.key(), "changeit".toCharArray(), new Certificate[]{cert.cert()}); try (FileOutputStream fos = new FileOutputStream(keyStorePkcs12Path)) { ks.store(fos, "changeit".toCharArray()); } @@ -108,6 +110,20 @@ public class ItSslClientHandlerTest { assertThrows(SocketException.class, this::performAndCheckMagic); } + @Test + @DisplayName("When SSL is configured incorrectly then exception is thrown on start") + @SuppressWarnings("ThrowableNotThrown") + void sslMisconfigured(TestInfo testInfo) { + testServer = new TestServer( + TestSslConfig.builder() + .keyStorePath(keyStorePkcs12Path) + .keyStorePassword("wrong-password") + .build() + ); + + assertThrowsWithCause(() -> testServer.start(testInfo), IgniteException.class, "keystore password was incorrect"); + } + private void performAndCheckMagic() throws IOException { int serverPort = serverModule.localAddress().getPort(); diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java index 9b20db8306..41e8f20848 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java @@ -189,6 +189,9 @@ public class ClientHandlerModule implements IgniteComponent { ServerBootstrap bootstrap = bootstrapFactory.createServerBootstrap(); + // Initialize SslContext once on startup to avoid initialization on each connection, and to fail in case of incorrect config. + SslContext sslContext = configuration.ssl().enabled() ? SslContextProvider.createServerSslContext(configuration.ssl()) : null; + bootstrap.childHandler(new ChannelInitializer<>() { @Override protected void initChannel(Channel ch) { @@ -200,9 +203,7 @@ public class ClientHandlerModule implements IgniteComponent { ch.pipeline().addLast(new IdleChannelHandler()); } - if (configuration.ssl().enabled()) { - SslContext sslContext = SslContextProvider.createServerSslContext(configuration.ssl()); - + if (sslContext != null) { ch.pipeline().addFirst("ssl", sslContext.newHandler(ch.alloc())); } diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj b/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj index 688b510889..75f7449b08 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj @@ -39,4 +39,13 @@ <ProjectReference Include="..\Apache.Ignite\Apache.Ignite.csproj" /> </ItemGroup> + <ItemGroup> + <None Update="truststore.pfx"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="keystore.pfx"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + </Project> diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs index 9a53221959..1d873755b8 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ClientSocketTests.cs @@ -33,7 +33,7 @@ namespace Apache.Ignite.Tests [Test] public async Task TestConnectAndSendRequestReturnsResponse() { - using var socket = await ClientSocket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, ServerPort), new(), _ => {}); + using var socket = await ClientSocket.ConnectAsync(GetEndPoint(), new(), _ => {}); using var requestWriter = ProtoCommon.GetMessageWriter(); requestWriter.MessageWriter.Write("non-existent-table"); @@ -45,7 +45,7 @@ namespace Apache.Ignite.Tests [Test] public async Task TestConnectAndSendRequestWithInvalidOpCodeThrowsError() { - using var socket = await ClientSocket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, ServerPort), new(), _ => {}); + using var socket = await ClientSocket.ConnectAsync(GetEndPoint(), new(), _ => {}); using var requestWriter = ProtoCommon.GetMessageWriter(); requestWriter.MessageWriter.Write(123); @@ -59,7 +59,7 @@ namespace Apache.Ignite.Tests [Test] public async Task TestDisposedSocketThrowsExceptionOnSend() { - var socket = await ClientSocket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, ServerPort), new(), _ => {}); + var socket = await ClientSocket.ConnectAsync(GetEndPoint(), new(), _ => {}); socket.Dispose(); @@ -78,7 +78,10 @@ namespace Apache.Ignite.Tests [Test] public void TestConnectWithoutServerThrowsException() { - Assert.CatchAsync(async () => await ClientSocket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 569), new(), _ => {})); + Assert.CatchAsync(async () => await ClientSocket.ConnectAsync(GetEndPoint(569), new(), _ => { })); } + + private static SocketEndpoint GetEndPoint(int? serverPort = null) => + new(new(IPAddress.Loopback, serverPort ?? ServerPort), string.Empty); } } diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs index 652dc89ae3..eac861de1b 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeClusterAwarenessTests.cs @@ -76,7 +76,7 @@ namespace Apache.Ignite.Tests.Compute Assert.IsEmpty(server2.ClientOps); Assert.IsEmpty(server3.ClientOps); - Assert.AreEqual(server1.Node, client.GetConnections().Single()); + Assert.AreEqual(server1.Node, client.GetConnections().Single().Node); } [Test] diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs index 5a14dcedf7..23fff5ffd6 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs @@ -60,7 +60,7 @@ namespace Apache.Ignite.Tests.Compute { var res = (await Client.GetClusterNodesAsync()).OrderBy(x => x.Name).ToList(); - Assert.AreEqual(2, res.Count); + Assert.AreEqual(4, res.Count); Assert.IsNotEmpty(res[0].Id); Assert.IsNotEmpty(res[1].Id); @@ -88,7 +88,11 @@ namespace Apache.Ignite.Tests.Compute { var res = await Client.Compute.ExecuteAsync<string>(await Client.GetClusterNodesAsync(), NodeNameJob); - CollectionAssert.Contains(new[] { PlatformTestNodeRunner, PlatformTestNodeRunner + "_2" }, res); + var expectedNodeNames = Enumerable.Range(1, 4) + .Select(x => x == 1 ? PlatformTestNodeRunner : PlatformTestNodeRunner + "_" + x) + .ToList(); + + CollectionAssert.Contains(expectedNodeNames, res); } [Test] @@ -120,11 +124,15 @@ namespace Apache.Ignite.Tests.Compute IDictionary<IClusterNode, Task<string>> taskMap = Client.Compute.BroadcastAsync<string>(nodes, NodeNameJob, "123"); var res1 = await taskMap[nodes[0]]; var res2 = await taskMap[nodes[1]]; + var res3 = await taskMap[nodes[2]]; + var res4 = await taskMap[nodes[3]]; - Assert.AreEqual(2, taskMap.Count); + Assert.AreEqual(4, taskMap.Count); Assert.AreEqual(nodes[0].Name + "123", res1); Assert.AreEqual(nodes[1].Name + "123", res2); + Assert.AreEqual(nodes[2].Name + "123", res3); + Assert.AreEqual(nodes[3].Name + "123", res4); } [Test] @@ -222,10 +230,11 @@ namespace Apache.Ignite.Tests.Compute } [Test] - [TestCase(1, "")] - [TestCase(2, "_2")] - [TestCase(3, "")] + [TestCase(1, "_4")] [TestCase(5, "_2")] + [TestCase(9, "_3")] + [TestCase(10, "")] + [TestCase(11, "_2")] public async Task TestExecuteColocated(long key, string nodeName) { var keyTuple = new IgniteTuple { [KeyCol] = key }; diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs index 67fd865944..16cb79de57 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/JavaServer.cs @@ -32,6 +32,7 @@ namespace Apache.Ignite.Tests public sealed class JavaServer : IDisposable { private const string GradleOptsEnvVar = "IGNITE_DOTNET_GRADLE_OPTS"; + private const string RequireExternalJavaServerEnvVar = "IGNITE_DOTNET_REQUIRE_EXTERNAL_SERVER"; private const int DefaultClientPort = 10942; @@ -67,6 +68,12 @@ namespace Apache.Ignite.Tests return new JavaServer(DefaultClientPort, null); } + if (bool.TryParse(Environment.GetEnvironmentVariable(RequireExternalJavaServerEnvVar), out var requireExternalServer) + && requireExternalServer) + { + throw new InvalidOperationException($"Java server is not started, but {RequireExternalJavaServerEnvVar} is set to true."); + } + Log(">>> Java server is not detected, starting..."); var process = CreateProcess(); diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/MultiClusterTest.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/MultiClusterTest.cs index 42848cdce4..9478150dba 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Tests/MultiClusterTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/MultiClusterTest.cs @@ -56,7 +56,7 @@ public class MultiClusterTest using var client = await IgniteClient.StartAsync(new IgniteClientConfiguration(server1.Endpoint, server2.Endpoint)); await client.Tables.GetTablesAsync(); - var primaryServer = client.GetConnections().Single().Address.Port == server1.Port ? server1 : server2; + var primaryServer = client.GetConnections().Single().Node.Address.Port == server1.Port ? server1 : server2; var secondaryServer = primaryServer == server1 ? server2 : server1; primaryServer.Dispose(); diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/SslTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/SslTests.cs new file mode 100644 index 0000000000..335733d086 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Tests/SslTests.cs @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +namespace Apache.Ignite.Tests; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Security.Authentication; +using System.Threading.Tasks; +using Log; +using NUnit.Framework; + +/// <summary> +/// SSL tests. +/// </summary> +public class SslTests : IgniteTestsBase +{ + private const string CertificatePassword = "123456"; + + private const string CertificateIssuer = + "E=d...@ignite.apache.org, OU=Apache Ignite CA, O=The Apache Software Foundation, CN=ignite.apache.org"; + + private static readonly string CertificatePath = Path.Combine( + TestUtils.RepoRootDir, "modules", "runner", "src", "integrationTest", "resources", "ssl", "client.pfx"); + + private static string SslEndpoint => "localhost:" + (ServerPort + 2); + + private static string SslEndpointWithClientAuth => "127.0.0.1:" + (ServerPort + 3); + + [Test] + [SuppressMessage("Security", "CA5398:Avoid hardcoded SslProtocols values", Justification = "Tests")] + public async Task TestSslWithoutClientAuthentication() + { + var cfg = new IgniteClientConfiguration + { + Endpoints = { SslEndpoint }, + SslStreamFactory = new SslStreamFactory { SkipServerCertificateValidation = true } + }; + + using var client = await IgniteClient.StartAsync(cfg); + + var connection = client.GetConnections().Single(); + var sslInfo = connection.SslInfo; + + Assert.IsNotNull(sslInfo); + Assert.IsFalse(sslInfo!.IsMutuallyAuthenticated); + Assert.AreEqual(TlsCipherSuite.TLS_AES_256_GCM_SHA384.ToString(), sslInfo.NegotiatedCipherSuiteName); + Assert.AreEqual(SslProtocols.Tls13, sslInfo.SslProtocol); + Assert.AreEqual("localhost", sslInfo.TargetHostName); + Assert.IsNull(sslInfo.LocalCertificate); + StringAssert.Contains(CertificateIssuer, sslInfo.RemoteCertificate!.Issuer); + } + + [Test] + public async Task TestSslWithClientAuthentication() + { + var cfg = new IgniteClientConfiguration + { + Endpoints = { SslEndpointWithClientAuth }, + SslStreamFactory = new SslStreamFactory + { + SkipServerCertificateValidation = true, + CertificatePath = CertificatePath, + CertificatePassword = CertificatePassword + }, + Logger = new ConsoleLogger { MinLevel = LogLevel.Trace } + }; + + using var client = await IgniteClient.StartAsync(cfg); + + var connection = client.GetConnections().Single(); + var sslInfo = connection.SslInfo; + + Assert.IsNotNull(sslInfo); + Assert.IsTrue(sslInfo!.IsMutuallyAuthenticated); + Assert.AreEqual(TlsCipherSuite.TLS_AES_256_GCM_SHA384.ToString(), sslInfo.NegotiatedCipherSuiteName); + Assert.AreEqual("127.0.0.1", sslInfo.TargetHostName); + StringAssert.Contains(CertificateIssuer, sslInfo.RemoteCertificate!.Issuer); + StringAssert.Contains(CertificateIssuer, sslInfo.LocalCertificate!.Issuer); + } + + [Test] + [SuppressMessage("Security", "CA5398:Avoid hardcoded SslProtocols values", Justification = "Tests.")] + public void TestSslOnClientWithoutSslOnServerThrows() + { + var cfg = GetConfig(); + cfg.SslStreamFactory = new SslStreamFactory + { + SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 + }; + + var ex = Assert.ThrowsAsync<AggregateException>(async () => await IgniteClient.StartAsync(cfg)); + Assert.IsInstanceOf<IgniteClientConnectionException>(ex?.InnerException); + } + + [Test] + public void TestSslOnServerWithoutSslOnClientThrows() + { + var cfg = new IgniteClientConfiguration + { + Endpoints = { SslEndpoint } + }; + + var ex = Assert.ThrowsAsync<AggregateException>(async () => await IgniteClient.StartAsync(cfg)); + Assert.IsInstanceOf<IgniteClientConnectionException>(ex?.InnerException); + } + + [Test] + public void TestMissingClientCertThrows() + { + var cfg = new IgniteClientConfiguration + { + Endpoints = { SslEndpointWithClientAuth }, + SslStreamFactory = new SslStreamFactory + { + SkipServerCertificateValidation = true, + CheckCertificateRevocation = true + } + }; + + Assert.CatchAsync<Exception>(async () => await IgniteClient.StartAsync(cfg)); + } + + [Test] + public async Task TestCustomSslStreamFactory() + { + var cfg = new IgniteClientConfiguration + { + Endpoints = { SslEndpoint }, + SslStreamFactory = new CustomSslStreamFactory() + }; + + using var client = await IgniteClient.StartAsync(cfg); + + var connection = client.GetConnections().Single(); + var sslInfo = connection.SslInfo; + + Assert.IsNotNull(sslInfo); + Assert.IsFalse(sslInfo!.IsMutuallyAuthenticated); + } + + [Test] + public async Task TestSslStreamFactoryReturnsNullDisablesSsl() + { + var cfg = GetConfig(); + cfg.SslStreamFactory = new NullSslStreamFactory(); + + using var client = await IgniteClient.StartAsync(cfg); + Assert.IsNull(client.GetConnections().Single().SslInfo); + } + + private class NullSslStreamFactory : ISslStreamFactory + { + public SslStream? Create(Stream stream, string targetHost) => null; + } + + private class CustomSslStreamFactory : ISslStreamFactory + { + public SslStream Create(Stream stream, string targetHost) + { + var sslStream = new SslStream( + innerStream: stream, + leaveInnerStreamOpen: false, + userCertificateValidationCallback: (_, certificate, _, _) => certificate!.Issuer.Contains("ignite"), + userCertificateSelectionCallback: null); + + sslStream.AuthenticateAsClient(targetHost, null, SslProtocols.None, false); + + return sslStream; + } + } +} diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/keystore.pfx b/modules/platforms/dotnet/Apache.Ignite.Tests/keystore.pfx new file mode 100644 index 0000000000..f448d29aba Binary files /dev/null and b/modules/platforms/dotnet/Apache.Ignite.Tests/keystore.pfx differ diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/truststore.pfx b/modules/platforms/dotnet/Apache.Ignite.Tests/truststore.pfx new file mode 100644 index 0000000000..ba842fd614 Binary files /dev/null and b/modules/platforms/dotnet/Apache.Ignite.Tests/truststore.pfx differ diff --git a/modules/platforms/dotnet/Apache.Ignite/IIgniteClient.cs b/modules/platforms/dotnet/Apache.Ignite/IIgniteClient.cs index 2ef60afff4..71de18fc2c 100644 --- a/modules/platforms/dotnet/Apache.Ignite/IIgniteClient.cs +++ b/modules/platforms/dotnet/Apache.Ignite/IIgniteClient.cs @@ -37,6 +37,6 @@ namespace Apache.Ignite /// Gets active connections. /// </summary> /// <returns>A list of connected cluster nodes.</returns> - IList<IClusterNode> GetConnections(); + IList<IConnectionInfo> GetConnections(); } } diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs b/modules/platforms/dotnet/Apache.Ignite/ISslStreamFactory.cs similarity index 58% copy from modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs copy to modules/platforms/dotnet/Apache.Ignite/ISslStreamFactory.cs index 48e0b7a194..6cd692b9ac 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs +++ b/modules/platforms/dotnet/Apache.Ignite/ISslStreamFactory.cs @@ -15,17 +15,25 @@ * limitations under the License. */ -namespace Apache.Ignite.Internal -{ - using System; - using Ignite.Network; +namespace Apache.Ignite; + +using System.IO; +using System.Net.Security; +/// <summary> +/// SSL Stream Factory defines how SSL connection is established. +/// <para /> +/// See <see cref="IgniteClientConfiguration.SslStreamFactory"/>, <see cref="SslStreamFactory"/>. +/// </summary> +public interface ISslStreamFactory +{ /// <summary> - /// Socket connection context. + /// Creates the SSL stream. /// </summary> - /// <param name="Version">Protocol version.</param> - /// <param name="IdleTimeout">Server idle timeout.</param> - /// <param name="ClusterNode">Cluster node.</param> - /// <param name="ClusterId">Cluster id.</param> - internal record ConnectionContext(ClientProtocolVersion Version, TimeSpan IdleTimeout, IClusterNode ClusterNode, Guid ClusterId); + /// <param name="stream">The underlying raw stream.</param> + /// <param name="targetHost">Target host.</param> + /// <returns> + /// SSL stream, or null if SSL is not enabled. + /// </returns> + SslStream? Create(Stream stream, string targetHost); } diff --git a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs index e428be881e..6ecfe08edc 100644 --- a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs +++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs @@ -86,6 +86,7 @@ namespace Apache.Ignite RetryPolicy = other.RetryPolicy; HeartbeatInterval = other.HeartbeatInterval; ReconnectInterval = other.ReconnectInterval; + SslStreamFactory = other.SslStreamFactory; } /// <summary> @@ -159,5 +160,14 @@ namespace Apache.Ignite /// </summary> [DefaultValue(typeof(TimeSpan), "00:00:30")] public TimeSpan ReconnectInterval { get; set; } = DefaultReconnectInterval; + + /// <summary> + /// Gets or sets the SSL stream factory. + /// <para /> + /// When not null, secure socket connection will be established. + /// <para /> + /// See <see cref="SslStreamFactory"/>. + /// </summary> + public ISslStreamFactory? SslStreamFactory { get; set; } } } diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs index b2ad854c86..356aed71a5 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientFailoverSocket.cs @@ -29,7 +29,9 @@ namespace Apache.Ignite.Internal using System.Threading; using System.Threading.Tasks; using Buffers; + using Ignite.Network; using Log; + using Network; using Proto; using Transactions; @@ -226,15 +228,15 @@ namespace Apache.Ignite.Internal /// Gets active connections. /// </summary> /// <returns>Active connections.</returns> - public IEnumerable<ConnectionContext> GetConnections() + public IList<IConnectionInfo> GetConnections() { - var res = new List<ConnectionContext>(_endpoints.Count); + var res = new List<IConnectionInfo>(_endpoints.Count); foreach (var endpoint in _endpoints) { if (endpoint.Socket is { IsDisposed: false, ConnectionContext: { } ctx }) { - res.Add(ctx); + res.Add(new ConnectionInfo(ctx.ClusterNode, ctx.SslInfo)); } } @@ -409,7 +411,7 @@ namespace Apache.Ignite.Internal try { - var socket = await ClientSocket.ConnectAsync(endpoint.EndPoint, Configuration, OnAssignmentChanged).ConfigureAwait(false); + var socket = await ClientSocket.ConnectAsync(endpoint, Configuration, OnAssignmentChanged).ConfigureAwait(false); if (_clusterId == null) { diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs index 2073291d04..66f9b09e19 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ClientSocket.cs @@ -22,12 +22,15 @@ namespace Apache.Ignite.Internal using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; + using System.IO; using System.Linq; using System.Net; + using System.Net.Security; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Buffers; + using Ignite.Network; using Log; using Network; using Proto; @@ -55,7 +58,7 @@ namespace Apache.Ignite.Internal private static long _socketId; /** Underlying stream. */ - private readonly NetworkStream _stream; + private readonly Stream _stream; /** Current async operations, map from request id. */ private readonly ConcurrentDictionary<long, TaskCompletionSource<PooledBuffer>> _requests = new(); @@ -107,7 +110,7 @@ namespace Apache.Ignite.Internal /// <param name="assignmentChangeCallback">Partition assignment change callback.</param> /// <param name="logger">Logger.</param> private ClientSocket( - NetworkStream stream, + Stream stream, IgniteClientConfiguration configuration, ConnectionContext connectionContext, Action<ClientSocket> assignmentChangeCallback, @@ -155,7 +158,7 @@ namespace Apache.Ignite.Internal "CA2000:Dispose objects before losing scope", Justification = "NetworkStream is returned from this method in the socket.")] public static async Task<ClientSocket> ConnectAsync( - IPEndPoint endPoint, + SocketEndpoint endPoint, IgniteClientConfiguration configuration, Action<ClientSocket> assignmentChangeCallback) { @@ -168,16 +171,35 @@ namespace Apache.Ignite.Internal try { - await socket.ConnectAsync(endPoint).ConfigureAwait(false); - logger?.Debug($"Socket connection established: {socket.LocalEndPoint} -> {socket.RemoteEndPoint}, starting handshake..."); + await socket.ConnectAsync(endPoint.EndPoint).ConfigureAwait(false); - var stream = new NetworkStream(socket, ownsSocket: true); + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.Debug( + $"Socket connection established: {socket.LocalEndPoint} -> {socket.RemoteEndPoint}, starting handshake..."); + } + + Stream stream = new NetworkStream(socket, ownsSocket: true); + + if (configuration.SslStreamFactory is { } sslStreamFactory && + sslStreamFactory.Create(stream, endPoint.Host) is { } sslStream) + { + stream = sslStream; + + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.Debug($"SSL connection established: {sslStream.NegotiatedCipherSuite}"); + } + } - var context = await HandshakeAsync(stream, endPoint) + var context = await HandshakeAsync(stream, endPoint.EndPoint) .WaitAsync(configuration.SocketTimeout) .ConfigureAwait(false); - logger?.Debug($"Handshake succeeded: {context}."); + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.Debug($"Handshake succeeded: {context}."); + } return new ClientSocket(stream, configuration, context, assignmentChangeCallback, logger); } @@ -188,7 +210,10 @@ namespace Apache.Ignite.Internal // ReSharper disable once MethodHasAsyncOverload socket.Dispose(); - throw new IgniteClientConnectionException(ErrorGroups.Client.Connection, "Failed to connect to endpoint: " + endPoint, e); + throw new IgniteClientConnectionException( + ErrorGroups.Client.Connection, + "Failed to connect to endpoint: " + endPoint.EndPoint, + e); } } @@ -261,7 +286,7 @@ namespace Apache.Ignite.Internal /// </summary> /// <param name="stream">Network stream.</param> /// <param name="endPoint">Endpoint.</param> - private static async Task<ConnectionContext> HandshakeAsync(NetworkStream stream, IPEndPoint endPoint) + private static async Task<ConnectionContext> HandshakeAsync(Stream stream, IPEndPoint endPoint) { await stream.WriteAsync(ProtoCommon.MagicBytes).ConfigureAwait(false); await WriteHandshakeAsync(stream, CurrentProtocolVersion).ConfigureAwait(false); @@ -271,10 +296,10 @@ namespace Apache.Ignite.Internal await CheckMagicBytesAsync(stream).ConfigureAwait(false); using var response = await ReadResponseAsync(stream, new byte[4], CancellationToken.None).ConfigureAwait(false); - return ReadHandshakeResponse(response.GetReader(), endPoint); + return ReadHandshakeResponse(response.GetReader(), endPoint, GetSslInfo(stream)); } - private static async ValueTask CheckMagicBytesAsync(NetworkStream stream) + private static async ValueTask CheckMagicBytesAsync(Stream stream) { var responseMagic = ByteArrayPool.Rent(ProtoCommon.MagicBytes.Length); @@ -298,7 +323,7 @@ namespace Apache.Ignite.Internal } } - private static ConnectionContext ReadHandshakeResponse(MsgPackReader reader, IPEndPoint endPoint) + private static ConnectionContext ReadHandshakeResponse(MsgPackReader reader, IPEndPoint endPoint, ISslInfo? sslInfo) { var serverVer = new ClientProtocolVersion(reader.ReadInt16(), reader.ReadInt16(), reader.ReadInt16()); @@ -326,7 +351,8 @@ namespace Apache.Ignite.Internal serverVer, TimeSpan.FromMilliseconds(idleTimeoutMs), new ClusterNode(clusterNodeId, clusterNodeName, endPoint), - clusterId); + clusterId, + sslInfo); } private static IgniteException? ReadError(ref MsgPackReader reader) @@ -346,7 +372,7 @@ namespace Apache.Ignite.Internal } private static async ValueTask<PooledBuffer> ReadResponseAsync( - NetworkStream stream, + Stream stream, byte[] messageSizeBytes, CancellationToken cancellationToken) { @@ -369,7 +395,7 @@ namespace Apache.Ignite.Internal } private static async Task<int> ReadMessageSizeAsync( - NetworkStream stream, + Stream stream, byte[] buffer, CancellationToken cancellationToken) { @@ -382,7 +408,7 @@ namespace Apache.Ignite.Internal } private static async Task ReceiveBytesAsync( - NetworkStream stream, + Stream stream, byte[] buffer, int size, CancellationToken cancellationToken) @@ -406,7 +432,7 @@ namespace Apache.Ignite.Internal } } - private static async ValueTask WriteHandshakeAsync(NetworkStream stream, ClientProtocolVersion version) + private static async ValueTask WriteHandshakeAsync(Stream stream, ClientProtocolVersion version) { using var bufferWriter = new PooledArrayBuffer(prefixSize: ProtoCommon.MessagePrefixSize); WriteHandshake(version, bufferWriter.MessageWriter); @@ -483,6 +509,17 @@ namespace Apache.Ignite.Internal return recommendedHeartbeatInterval; } + private static ISslInfo? GetSslInfo(Stream stream) => + stream is SslStream sslStream + ? new SslInfo( + sslStream.TargetHostName, + sslStream.NegotiatedCipherSuite.ToString(), + sslStream.IsMutuallyAuthenticated, + sslStream.LocalCertificate, + sslStream.RemoteCertificate, + sslStream.SslProtocol) + : null; + private async ValueTask SendRequestAsync(PooledArrayBuffer? request, ClientOp op, long requestId) { // Reset heartbeat timer - don't sent heartbeats when connection is active anyway. diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs index 48e0b7a194..c785dacb77 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs @@ -27,5 +27,11 @@ namespace Apache.Ignite.Internal /// <param name="IdleTimeout">Server idle timeout.</param> /// <param name="ClusterNode">Cluster node.</param> /// <param name="ClusterId">Cluster id.</param> - internal record ConnectionContext(ClientProtocolVersion Version, TimeSpan IdleTimeout, IClusterNode ClusterNode, Guid ClusterId); + /// <param name="SslInfo">SSL info.</param> + internal record ConnectionContext( + ClientProtocolVersion Version, + TimeSpan IdleTimeout, + IClusterNode ClusterNode, + Guid ClusterId, + ISslInfo? SslInfo); } diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs index 896b2bda84..d71548cf61 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs @@ -18,7 +18,6 @@ namespace Apache.Ignite.Internal { using System.Collections.Generic; - using System.Linq; using System.Net; using System.Threading.Tasks; using Ignite.Compute; @@ -97,8 +96,7 @@ namespace Apache.Ignite.Internal } /// <inheritdoc/> - public IList<IClusterNode> GetConnections() => - _socket.GetConnections().Select(ctx => ctx.ClusterNode).ToList(); + public IList<IConnectionInfo> GetConnections() => _socket.GetConnections(); /// <inheritdoc/> public void Dispose() diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Network/ConnectionInfo.cs similarity index 60% copy from modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs copy to modules/platforms/dotnet/Apache.Ignite/Internal/Network/ConnectionInfo.cs index 48e0b7a194..ec41b99d6d 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Network/ConnectionInfo.cs @@ -15,17 +15,8 @@ * limitations under the License. */ -namespace Apache.Ignite.Internal -{ - using System; - using Ignite.Network; +namespace Apache.Ignite.Internal.Network; - /// <summary> - /// Socket connection context. - /// </summary> - /// <param name="Version">Protocol version.</param> - /// <param name="IdleTimeout">Server idle timeout.</param> - /// <param name="ClusterNode">Cluster node.</param> - /// <param name="ClusterId">Cluster id.</param> - internal record ConnectionContext(ClientProtocolVersion Version, TimeSpan IdleTimeout, IClusterNode ClusterNode, Guid ClusterId); -} +using Ignite.Network; + +internal sealed record ConnectionInfo(IClusterNode Node, ISslInfo? SslInfo) : IConnectionInfo; diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Network/SslInfo.cs similarity index 60% copy from modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs copy to modules/platforms/dotnet/Apache.Ignite/Internal/Network/SslInfo.cs index 48e0b7a194..0ff0f8e489 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Network/SslInfo.cs @@ -15,17 +15,16 @@ * limitations under the License. */ -namespace Apache.Ignite.Internal -{ - using System; - using Ignite.Network; +namespace Apache.Ignite.Internal.Network; - /// <summary> - /// Socket connection context. - /// </summary> - /// <param name="Version">Protocol version.</param> - /// <param name="IdleTimeout">Server idle timeout.</param> - /// <param name="ClusterNode">Cluster node.</param> - /// <param name="ClusterId">Cluster id.</param> - internal record ConnectionContext(ClientProtocolVersion Version, TimeSpan IdleTimeout, IClusterNode ClusterNode, Guid ClusterId); -} +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Ignite.Network; + +internal sealed record SslInfo( + string TargetHostName, + string NegotiatedCipherSuiteName, + bool IsMutuallyAuthenticated, + X509Certificate? LocalCertificate, + X509Certificate? RemoteCertificate, + SslProtocols SslProtocol) : ISslInfo; diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs b/modules/platforms/dotnet/Apache.Ignite/Network/IConnectionInfo.cs similarity index 64% copy from modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs copy to modules/platforms/dotnet/Apache.Ignite/Network/IConnectionInfo.cs index 48e0b7a194..2025f43bb4 100644 --- a/modules/platforms/dotnet/Apache.Ignite/Internal/ConnectionContext.cs +++ b/modules/platforms/dotnet/Apache.Ignite/Network/IConnectionInfo.cs @@ -15,17 +15,20 @@ * limitations under the License. */ -namespace Apache.Ignite.Internal +namespace Apache.Ignite.Network; + +/// <summary> +/// Connection info. +/// </summary> +public interface IConnectionInfo { - using System; - using Ignite.Network; + /// <summary> + /// Gets the target node. + /// </summary> + IClusterNode Node { get; } /// <summary> - /// Socket connection context. + /// Gets the SSL info, if SSL is enabled. /// </summary> - /// <param name="Version">Protocol version.</param> - /// <param name="IdleTimeout">Server idle timeout.</param> - /// <param name="ClusterNode">Cluster node.</param> - /// <param name="ClusterId">Cluster id.</param> - internal record ConnectionContext(ClientProtocolVersion Version, TimeSpan IdleTimeout, IClusterNode ClusterNode, Guid ClusterId); + ISslInfo? SslInfo { get; } } diff --git a/modules/platforms/dotnet/Apache.Ignite/Network/ISslInfo.cs b/modules/platforms/dotnet/Apache.Ignite/Network/ISslInfo.cs new file mode 100644 index 0000000000..65e49a9d9a --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite/Network/ISslInfo.cs @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +namespace Apache.Ignite.Network; + +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +/// <summary> +/// SSL info. +/// </summary> +public interface ISslInfo +{ + /// <summary> + /// Gets the cipher suite which was negotiated for this connection. + /// </summary> + string NegotiatedCipherSuiteName { get; } + + /// <summary> + /// Gets the certificate used to authenticate the local endpoint. + /// </summary> + X509Certificate? LocalCertificate { get; } + + /// <summary> + /// Gets the certificate used to authenticate the remote endpoint. + /// </summary> + X509Certificate? RemoteCertificate { get; } + + /// <summary> + /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. + /// It can be a DNS name or an IP address. + /// </summary> + string TargetHostName { get; } + + /// <summary> + /// Gets a value indicating whether both server and client have been authenticated. + /// </summary> + bool IsMutuallyAuthenticated { get; } + + /// <summary> + /// Gets the SSL protocol. + /// </summary> + SslProtocols SslProtocol { get; } +} diff --git a/modules/platforms/dotnet/Apache.Ignite/SslStreamFactory.cs b/modules/platforms/dotnet/Apache.Ignite/SslStreamFactory.cs new file mode 100644 index 0000000000..02f552260d --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite/SslStreamFactory.cs @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +namespace Apache.Ignite; + +using System.ComponentModel; +using System.IO; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Internal.Common; + +/// <summary> +/// Standard SSL stream factory. Can be used with or without client-side certificates. +/// </summary> +public sealed class SslStreamFactory : ISslStreamFactory +{ + /// <summary> + /// Default SSL protocols. + /// </summary> + public const SslProtocols DefaultSslProtocols = SslProtocols.None; + + /// <summary> + /// Gets or sets the certificate file path (see <see cref="X509Certificate2"/>). + /// </summary> + public string? CertificatePath { get; set; } + + /// <summary> + /// Gets or sets the certificate file password (see <see cref="X509Certificate2"/>). + /// </summary> + public string? CertificatePassword { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to ignore invalid remote (server) certificates. + /// This may be useful for testing with self-signed certificates. + /// </summary> + public bool SkipServerCertificateValidation { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the certificate revocation list is checked during authentication. + /// </summary> + public bool CheckCertificateRevocation { get; set; } + + /// <summary> + /// Gets or sets the SSL protocols. + /// </summary> + [DefaultValue(DefaultSslProtocols)] + public SslProtocols SslProtocols { get; set; } = DefaultSslProtocols; + + /// <inheritdoc /> + public SslStream Create(Stream stream, string targetHost) + { + IgniteArgumentCheck.NotNull(stream, "stream"); + + var sslStream = new SslStream(stream, false, ValidateServerCertificate, null); + + var cert = string.IsNullOrEmpty(CertificatePath) + ? null + : new X509Certificate2(CertificatePath, CertificatePassword); + + var certs = cert == null + ? null + : new X509CertificateCollection(new X509Certificate[] { cert }); + + sslStream.AuthenticateAsClient(targetHost, certs, SslProtocols, CheckCertificateRevocation); + + return sslStream; + } + + /// <summary> + /// Validates the server certificate. + /// </summary> + private bool ValidateServerCertificate( + object sender, + X509Certificate? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + if (SkipServerCertificateValidation) + { + return true; + } + + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + return false; + } +} diff --git a/modules/platforms/dotnet/DEVNOTES.md b/modules/platforms/dotnet/DEVNOTES.md index ddd5d9a7b6..61d55c75b5 100644 --- a/modules/platforms/dotnet/DEVNOTES.md +++ b/modules/platforms/dotnet/DEVNOTES.md @@ -26,6 +26,8 @@ then run .NET tests with `dotnet test` or `dotnet test --filter TEST_NAME`. When The test node will stop after 30 minutes by default. To change this, set `IGNITE_PLATFORM_TEST_NODE_RUNNER_RUN_TIME_MINUTES` environment variable. +To ensure that external test node is used, and a new one is never started by .NET test code, set `IGNITE_DOTNET_REQUIRE_EXTERNAL_SERVER` to `true`. + ## Static Code Analysis Static code analysis (Roslyn-based) runs as part of the build and includes code style check. Build fails on any warning. diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java index 808048da9a..4e225550c7 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java @@ -46,6 +46,7 @@ import org.apache.ignite.internal.schema.testutils.builder.SchemaBuilders; import org.apache.ignite.internal.schema.testutils.definition.ColumnType; import org.apache.ignite.internal.schema.testutils.definition.ColumnType.TemporalColumnType; import org.apache.ignite.internal.schema.testutils.definition.TableDefinition; +import org.apache.ignite.internal.ssl.ItSslTest; import org.apache.ignite.internal.table.distributed.TableManager; import org.apache.ignite.internal.table.impl.DummySchemaManagerImpl; import org.apache.ignite.internal.util.IgniteUtils; @@ -63,6 +64,12 @@ public class PlatformTestNodeRunner { /** Test node name 2. */ private static final String NODE_NAME2 = PlatformTestNodeRunner.class.getCanonicalName() + "_2"; + /** Test node name 3. */ + private static final String NODE_NAME3 = PlatformTestNodeRunner.class.getCanonicalName() + "_3"; + + /** Test node name 4. */ + private static final String NODE_NAME4 = PlatformTestNodeRunner.class.getCanonicalName() + "_4"; + private static final String SCHEMA_NAME = "PUBLIC"; private static final String TABLE_NAME = "TBL1"; @@ -80,23 +87,72 @@ public class PlatformTestNodeRunner { /** Nodes bootstrap configuration. */ private static final Map<String, String> nodesBootstrapCfg = Map.of( NODE_NAME, "{\n" - + " \"clientConnector\":{\"port\": 10942,\"portRange\":10,\"idleTimeout\":3000,\"" + + " \"clientConnector\":{\"port\": 10942,\"portRange\":1,\"idleTimeout\":3000,\"" + "sendServerExceptionStackTraceToClient\":true}," + " \"network\": {\n" + " \"port\":3344,\n" + " \"nodeFinder\": {\n" - + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\" ]\n" + + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\", \"localhost:3347\" ]\n" + " }\n" + " }\n" + "}", NODE_NAME2, "{\n" - + " \"clientConnector\":{\"port\": 10942,\"portRange\":10,\"idleTimeout\":3000," + + " \"clientConnector\":{\"port\": 10943,\"portRange\":1,\"idleTimeout\":3000," + "\"sendServerExceptionStackTraceToClient\":true}," + " \"network\": {\n" + " \"port\":3345,\n" + " \"nodeFinder\": {\n" - + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\" ]\n" + + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\", \"localhost:3347\" ]\n" + + " }\n" + + " }\n" + + "}", + + NODE_NAME3, "{\n" + + " \"clientConnector\":{" + + " \"port\": 10944," + + " \"portRange\":1," + + " \"idleTimeout\":3000," + + " \"sendServerExceptionStackTraceToClient\":true, " + + " \"ssl\": {\n" + + " enabled: true,\n" + + " keyStore: {\n" + + " path: \"KEYSTORE_PATH\",\n" + + " password: \"SSL_STORE_PASS\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"network\": {\n" + + " \"port\":3346,\n" + + " \"nodeFinder\": {\n" + + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\", \"localhost:3347\" ]\n" + + " }\n" + + " }\n" + + "}", + + NODE_NAME4, "{\n" + + " \"clientConnector\":{" + + " \"port\": 10945," + + " \"portRange\":1," + + " \"idleTimeout\":3000," + + " \"sendServerExceptionStackTraceToClient\":true, " + + " \"ssl\": {\n" + + " enabled: true,\n" + + " clientAuth: \"require\",\n" + + " keyStore: {\n" + + " path: \"KEYSTORE_PATH\",\n" + + " password: \"SSL_STORE_PASS\"\n" + + " },\n" + + " trustStore: {\n" + + " path: \"TRUSTSTORE_PATH\",\n" + + " password: \"SSL_STORE_PASS\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"network\": {\n" + + " \"port\":3347,\n" + + " \"nodeFinder\": {\n" + + " \"netClusterNodes\":[ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\", \"localhost:3347\" ]\n" + " }\n" + " }\n" + "}" @@ -125,10 +181,17 @@ public class PlatformTestNodeRunner { IgniteUtils.deleteIfExists(BASE_PATH); Files.createDirectories(BASE_PATH); + var sslPassword = "123456"; + var trustStorePath = ItSslTest.class.getClassLoader().getResource("ssl/trust.jks").getPath(); + var keyStorePath = ItSslTest.class.getClassLoader().getResource("ssl/server.jks").getPath(); + List<CompletableFuture<Ignite>> igniteFutures = nodesBootstrapCfg.entrySet().stream() .map(e -> { String nodeName = e.getKey(); - String config = e.getValue(); + String config = e.getValue() + .replace("KEYSTORE_PATH", keyStorePath) + .replace("TRUSTSTORE_PATH", trustStorePath) + .replace("SSL_STORE_PASS", sslPassword); return IgnitionManager.start(nodeName, config, BASE_PATH.resolve(nodeName)); }) diff --git a/modules/runner/src/integrationTest/resources/ssl/client.pfx b/modules/runner/src/integrationTest/resources/ssl/client.pfx new file mode 100644 index 0000000000..29efdce5a4 Binary files /dev/null and b/modules/runner/src/integrationTest/resources/ssl/client.pfx differ diff --git a/modules/runner/src/integrationTest/resources/ssl/server.jks b/modules/runner/src/integrationTest/resources/ssl/server.jks new file mode 100644 index 0000000000..7007256253 Binary files /dev/null and b/modules/runner/src/integrationTest/resources/ssl/server.jks differ diff --git a/modules/runner/src/integrationTest/resources/ssl/trust.jks b/modules/runner/src/integrationTest/resources/ssl/trust.jks new file mode 100644 index 0000000000..e363af2eb8 Binary files /dev/null and b/modules/runner/src/integrationTest/resources/ssl/trust.jks differ