IGNITE-8042: .NET thin client: authentication support. This closes #3790.
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/67023a88 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/67023a88 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/67023a88 Branch: refs/heads/ignite-7708 Commit: 67023a88b8c9cf647196b6deb1759b4d01e33890 Parents: e333f30 Author: devozerov <voze...@gridgain.com> Authored: Thu Apr 12 10:37:36 2018 +0300 Committer: devozerov <voze...@gridgain.com> Committed: Thu Apr 12 10:37:36 2018 +0300 ---------------------------------------------------------------------- .../client/ClientAuthenticationException.java | 16 +- .../internal/client/thin/TcpClientChannel.java | 9 +- .../odbc/ClientListenerNioListener.java | 19 +++ .../client/ClientConnectionContext.java | 8 +- .../platform/client/ClientStatus.java | 5 +- .../Client/ClientConnectionTest.cs | 161 +++++++++++++++++++ .../Client/ClientStatusCode.cs | 12 +- .../Client/IgniteClientConfiguration.cs | 13 ++ .../IgniteClientConfigurationSection.xsd | 10 ++ .../Impl/Client/ClientProtocolVersion.cs | 22 ++- .../Impl/Client/ClientSocket.cs | 85 +++++++++- 11 files changed, 329 insertions(+), 31 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/core/src/main/java/org/apache/ignite/client/ClientAuthenticationException.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientAuthenticationException.java b/modules/core/src/main/java/org/apache/ignite/client/ClientAuthenticationException.java index 0c24db8..526690a 100644 --- a/modules/core/src/main/java/org/apache/ignite/client/ClientAuthenticationException.java +++ b/modules/core/src/main/java/org/apache/ignite/client/ClientAuthenticationException.java @@ -24,22 +24,10 @@ public class ClientAuthenticationException extends ClientException { /** Serial version uid. */ private static final long serialVersionUID = 0L; - /** Message. */ - private static final String MSG = "Invalid user name or password"; - /** * Default constructor. */ - public ClientAuthenticationException() { - super(MSG); - } - - /** - * Constructs a new exception with the specified cause. - * - * @param cause the cause. - */ - public ClientAuthenticationException(Throwable cause) { - super(MSG, cause); + public ClientAuthenticationException(String msg) { + super(msg); } } http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java index 8e8294f..10dc865 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientChannel.java @@ -272,8 +272,13 @@ class TcpClientChannel implements ClientChannel { try (BinaryReaderExImpl r = new BinaryReaderExImpl(null, res, null, true)) { String err = r.readString(); - if (err != null && err.toUpperCase().matches(".*USER.*INCORRECT.*")) - throw new ClientAuthenticationException(); + int errCode = ClientStatus.FAILED; + + if (res.remaining() > 0) + errCode = r.readInt(); + + if (errCode == ClientStatus.AUTH_FAILED) + throw new ClientAuthenticationException(err); else if (ver.equals(srvVer)) throw new ClientProtocolError(err); else if (!supportedVers.contains(srvVer) || http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java index 53b14d7..407c1a0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerNioListener.java @@ -26,9 +26,11 @@ import org.apache.ignite.internal.binary.BinaryWriterExImpl; import org.apache.ignite.internal.binary.streams.BinaryHeapInputStream; import org.apache.ignite.internal.binary.streams.BinaryHeapOutputStream; import org.apache.ignite.internal.binary.streams.BinaryInputStream; +import org.apache.ignite.internal.processors.authentication.IgniteAccessControlException; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcConnectionContext; import org.apache.ignite.internal.processors.odbc.odbc.OdbcConnectionContext; import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext; +import org.apache.ignite.internal.processors.platform.client.ClientStatus; import org.apache.ignite.internal.util.GridSpinBusyLock; import org.apache.ignite.internal.util.nio.GridNioServerListenerAdapter; import org.apache.ignite.internal.util.nio.GridNioSession; @@ -228,6 +230,18 @@ public class ClientListenerNioListener extends GridNioServerListenerAdapter<byte connCtx.handler().writeHandshake(writer); } + catch (IgniteAccessControlException authEx) { + writer.writeBoolean(false); + + writer.writeShort((short)0); + writer.writeShort((short)0); + writer.writeShort((short)0); + + writer.doWriteString(authEx.getMessage()); + + if (ver.compareTo(ClientConnectionContext.VER_1_1_0) >= 0) + writer.writeInt(ClientStatus.AUTH_FAILED); + } catch (IgniteCheckedException e) { U.warn(log, "Error during handshake [rmtAddr=" + ses.remoteAddress() + ", msg=" + e.getMessage() + ']'); @@ -239,10 +253,15 @@ public class ClientListenerNioListener extends GridNioServerListenerAdapter<byte currVer = connCtx.currentVersion(); writer.writeBoolean(false); + writer.writeShort(currVer.major()); writer.writeShort(currVer.minor()); writer.writeShort(currVer.maintenance()); + writer.doWriteString(e.getMessage()); + + if (ver.compareTo(ClientConnectionContext.VER_1_1_0) >= 0) + writer.writeInt(ClientStatus.FAILED); } ses.send(writer.array()); http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientConnectionContext.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientConnectionContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientConnectionContext.java index 061aab3..056ea83 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientConnectionContext.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientConnectionContext.java @@ -44,10 +44,10 @@ import static org.apache.ignite.plugin.security.SecuritySubjectType.REMOTE_CLIEN */ public class ClientConnectionContext implements ClientListenerConnectionContext { /** Version 1.0.0. */ - private static final ClientListenerProtocolVersion VER_1_0_0 = ClientListenerProtocolVersion.create(1, 0, 0); + public static final ClientListenerProtocolVersion VER_1_0_0 = ClientListenerProtocolVersion.create(1, 0, 0); /** Version 1.1.0. */ - private static final ClientListenerProtocolVersion VER_1_1_0 = ClientListenerProtocolVersion.create(1, 1, 0); + public static final ClientListenerProtocolVersion VER_1_1_0 = ClientListenerProtocolVersion.create(1, 1, 0); /** Supported versions. */ private static final Collection<ClientListenerProtocolVersion> SUPPORTED_VERS = Arrays.asList(VER_1_1_0, VER_1_0_0); @@ -144,12 +144,12 @@ public class ClientConnectionContext implements ClientListenerConnectionContext authCtx = thirdPartyAuthentication(user, pwd).authorizationContext(); else if (kernalCtx.authentication().enabled()) { if (user == null || user.length() == 0) - throw new IgniteCheckedException("Unauthenticated sessions are prohibited."); + throw new IgniteAccessControlException("Unauthenticated sessions are prohibited."); authCtx = kernalCtx.authentication().authenticate(user, pwd); if (authCtx == null) - throw new IgniteCheckedException("Unknown authentication error."); + throw new IgniteAccessControlException("Unknown authentication error."); } handler = new ClientRequestHandler(this, authCtx); http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientStatus.java ---------------------------------------------------------------------- diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientStatus.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientStatus.java index b8dfb1f..e63812c 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientStatus.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientStatus.java @@ -49,6 +49,9 @@ public final class ClientStatus { /** Resource does not exist. */ public static final int RESOURCE_DOES_NOT_EXIST = 1011; - /** Resource does not exist. */ + /** Authorization failure. */ public static final int SECURITY_VIOLATION = 1012; + + /** Authentication failed. */ + public static final int AUTH_FAILED = 2000; } http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs index 9da9a03..2ea17a8 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Client/ClientConnectionTest.cs @@ -19,13 +19,17 @@ namespace Apache.Ignite.Core.Tests.Client { using System; using System.Collections.Generic; + using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; + using Apache.Ignite.Core.Cache.Configuration; + using Apache.Ignite.Core.Cache.Query; using Apache.Ignite.Core.Client; + using Apache.Ignite.Core.Client.Cache; using Apache.Ignite.Core.Configuration; using NUnit.Framework; @@ -34,6 +38,18 @@ namespace Apache.Ignite.Core.Tests.Client /// </summary> public class ClientConnectionTest { + /** Temp dir for WAL. */ + private readonly string _tempDir = TestUtils.GetTempDirectoryName(); + + /// <summary> + /// Sets up the test. + /// </summary> + [SetUp] + public void SetUp() + { + TestUtils.ClearWorkDir(); + } + /// <summary> /// Fixture tear down. /// </summary> @@ -41,6 +57,13 @@ namespace Apache.Ignite.Core.Tests.Client public void TearDown() { Ignition.StopAll(true); + + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + + TestUtils.ClearWorkDir(); } /// <summary> @@ -55,6 +78,107 @@ namespace Apache.Ignite.Core.Tests.Client } /// <summary> + /// Tests that empty username or password are not allowed. + /// </summary> + [Test] + public void TestAuthenticationEmptyCredentials() + { + using (Ignition.Start(SecureServerConfig())) + { + var cliCfg = SecureClientConfig(); + + cliCfg.Password = null; + var ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.IsTrue(ex.Message.StartsWith("IgniteClientConfiguration.Password cannot be null")); + + cliCfg.Password = ""; + ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.IsTrue(ex.Message.StartsWith("IgniteClientConfiguration.Password cannot be empty")); + + cliCfg.Password = "ignite"; + + cliCfg.Username = null; + ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.IsTrue(ex.Message.StartsWith("IgniteClientConfiguration.Username cannot be null")); + + cliCfg.Username = ""; + ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.IsTrue(ex.Message.StartsWith("IgniteClientConfiguration.Username cannot be empty")); + } + } + + /// <summary> + /// Test invalid username or password. + /// </summary> + [Test] + public void TestAuthenticationInvalidCredentials() + { + using (Ignition.Start(SecureServerConfig())) + { + var cliCfg = SecureClientConfig(); + + cliCfg.Username = "invalid"; + + var ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.True(ex.StatusCode == ClientStatusCode.AuthenticationFailed); + + cliCfg.Username = "ignite"; + cliCfg.Password = "invalid"; + + ex = Assert.Throws<IgniteClientException>(() => { Ignition.StartClient(cliCfg); }); + Assert.True(ex.StatusCode == ClientStatusCode.AuthenticationFailed); + } + } + + /// <summary> + /// Test authentication. + /// </summary> + [Test] + public void TestAuthentication() + { + using (var srv = Ignition.Start(SecureServerConfig())) + { + srv.GetCluster().SetActive(true); + + using (var cli = Ignition.StartClient(SecureClientConfig())) + { + CacheClientConfiguration ccfg = new CacheClientConfiguration() + { + Name = "TestCache", + QueryEntities = new[] + { + new QueryEntity + { + KeyType = typeof(string), + ValueType = typeof(string), + }, + }, + }; + + ICacheClient<string, string> cache = cli.GetOrCreateCache<string, string>(ccfg); + + cache.Put("key1", "val1"); + + cache.Query(new SqlFieldsQuery("CREATE USER \"my_User\" WITH PASSWORD 'my_Password'")).GetAll(); + } + + var cliCfg = SecureClientConfig(); + + cliCfg.Username = "my_User"; + cliCfg.Password = "my_Password"; + + using (var cli = Ignition.StartClient(cliCfg)) + { + ICacheClient<string, string> cache = cli.GetCache<string, string>("TestCache"); + + string val = cache.Get("key1"); + + Assert.True(val == "val1"); + } + } + } + + /// <summary> /// Tests that multiple clients can connect to one server. /// </summary> [Test] @@ -374,5 +498,42 @@ namespace Apache.Ignite.Core.Tests.Client throw new Exception("SocketException not found.", origEx); } + + /// <summary> + /// Create server configuration with enabled authentication. + /// </summary> + /// <returns>Server configuration.</returns> + private IgniteConfiguration SecureServerConfig() + { + return new IgniteConfiguration(TestUtils.GetTestConfiguration()) + { + AuthenticationEnabled = true, + DataStorageConfiguration = new DataStorageConfiguration() + { + StoragePath = Path.Combine(_tempDir, "Store"), + WalPath = Path.Combine(_tempDir, "WalStore"), + WalArchivePath = Path.Combine(_tempDir, "WalArchive"), + DefaultDataRegionConfiguration = new DataRegionConfiguration() + { + Name = "default", + PersistenceEnabled = true + } + } + }; + } + + /// <summary> + /// Create client configuration with enabled authentication. + /// </summary> + /// <returns>Client configuration.</returns> + private static IgniteClientConfiguration SecureClientConfig() + { + return new IgniteClientConfiguration() + { + Host = "localhost", + Username = "ignite", + Password = "ignite" + }; + } } } http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientStatusCode.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientStatusCode.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientStatusCode.cs index 3f5ee8e..3bdd9e1 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientStatusCode.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/ClientStatusCode.cs @@ -52,6 +52,16 @@ namespace Apache.Ignite.Core.Client /// <summary> /// The too many cursors (see <see cref="ClientConnectorConfiguration.MaxOpenCursorsPerConnection"/>). /// </summary> - TooManyCursors = 1010 + TooManyCursors = 1010, + + /// <summary> + /// Authorization failure. + /// </summary> + SecurityViolation = 1012, + + /// <summary> + /// Authentication failed. + /// </summary> + AuthenticationFailed = 2000 } } http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs index 8730f39..3252495 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Client/IgniteClientConfiguration.cs @@ -90,6 +90,9 @@ namespace Apache.Ignite.Core.Client BinaryProcessor = cfg.BinaryProcessor; SslStreamFactory = cfg.SslStreamFactory; + + Username = cfg.Username; + Password = cfg.Password; } /// <summary> @@ -146,6 +149,16 @@ namespace Apache.Ignite.Core.Client public ISslStreamFactory SslStreamFactory { get; set; } /// <summary> + /// Username to be used to connect to secured cluster. + /// </summary> + public string Username { get; set; } + + /// <summary> + /// Password to be used to connect to secured cluster. + /// </summary> + public string Password { get; set; } + + /// <summary> /// Gets or sets custom binary processor. Internal property for tests. /// </summary> internal IBinaryProcessor BinaryProcessor { get; set; } http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd index 569ee6f..7e6caff 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd +++ b/modules/platforms/dotnet/Apache.Ignite.Core/IgniteClientConfigurationSection.xsd @@ -237,6 +237,16 @@ <xs:documentation>Socket operation timeout. Zero or negative for infinite timeout.</xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute name="username" type="xs:string"> + <xs:annotation> + <xs:documentation>Username to be used to connect to secured cluster.</xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="password" type="xs:string"> + <xs:annotation> + <xs:documentation>Password to be used to connect to secured cluster.</xs:documentation> + </xs:annotation> + </xs:attribute> </xs:complexType> </xs:element> </xs:schema> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientProtocolVersion.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientProtocolVersion.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientProtocolVersion.cs index bfdf5a3..4fe5c71 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientProtocolVersion.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientProtocolVersion.cs @@ -22,7 +22,7 @@ namespace Apache.Ignite.Core.Impl.Client /// <summary> /// Client protocol version. /// </summary> - internal struct ClientProtocolVersion : IEquatable<ClientProtocolVersion> + internal struct ClientProtocolVersion : IEquatable<ClientProtocolVersion>, IComparable<ClientProtocolVersion> { /** */ private readonly short _major; @@ -68,6 +68,26 @@ namespace Apache.Ignite.Core.Impl.Client } /// <summary> + /// Compare this version to other version. + /// </summary> + /// <param name="other"></param> + /// <returns></returns> + public int CompareTo(ClientProtocolVersion other) + { + int res = Major - other.Major; + + if (res == 0) + { + res = Minor - other.Minor; + + if (res == 0) + res = Maintenance - other.Maintenance; + } + + return res; + } + + /// <summary> /// Returns a value indicating whether specified instance equals to current. /// </summary> public bool Equals(ClientProtocolVersion other) http://git-wip-us.apache.org/repos/asf/ignite/blob/67023a88/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs index fca5dab..27d8f0b 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Client/ClientSocket.cs @@ -28,6 +28,7 @@ namespace Apache.Ignite.Core.Impl.Client using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; + using System.Xml.Schema; using Apache.Ignite.Core.Client; using Apache.Ignite.Core.Common; using Apache.Ignite.Core.Impl.Binary; @@ -38,8 +39,14 @@ namespace Apache.Ignite.Core.Impl.Client /// </summary> internal sealed class ClientSocket : IDisposable { + /** Version 1.0.0. */ + private static readonly ClientProtocolVersion Ver100 = new ClientProtocolVersion(1, 0, 0); + + /** Version 1.1.0. */ + private static readonly ClientProtocolVersion Ver110 = new ClientProtocolVersion(1, 1, 0); + /** Current version. */ - private static readonly ClientProtocolVersion CurrentProtocolVersion = new ClientProtocolVersion(1, 0, 0); + private static readonly ClientProtocolVersion CurrentProtocolVersion = Ver110; /** Handshake opcode. */ private const byte OpHandshake = 1; @@ -98,7 +105,9 @@ namespace Apache.Ignite.Core.Impl.Client _socket = Connect(clientConfiguration); _stream = GetSocketStream(_socket, clientConfiguration); - Handshake(version ?? CurrentProtocolVersion); + Validate(clientConfiguration); + + Handshake(clientConfiguration, version ?? CurrentProtocolVersion); // Check periodically if any request has timed out. if (_timeout > TimeSpan.Zero) @@ -112,6 +121,31 @@ namespace Apache.Ignite.Core.Impl.Client } /// <summary> + /// Validate configuration. + /// </summary> + /// <param name="cfg">Configuration.</param> + private void Validate(IgniteClientConfiguration cfg) + { + if (cfg.Username != null) + { + if (cfg.Username.Length == 0) + throw new IgniteClientException("IgniteClientConfiguration.Username cannot be empty."); + + if (cfg.Password == null) + throw new IgniteClientException("IgniteClientConfiguration.Password cannot be null when Username is set."); + } + + if (cfg.Password != null) + { + if (cfg.Password.Length == 0) + throw new IgniteClientException("IgniteClientConfiguration.Password cannot be empty."); + + if (cfg.Username == null) + throw new IgniteClientException("IgniteClientConfiguration.Username cannot be null when Password is set."); + } + } + + /// <summary> /// Performs a send-receive operation. /// </summary> public T DoOutInOp<T>(ClientOp opId, Action<IBinaryStream> writeAction, @@ -226,8 +260,10 @@ namespace Apache.Ignite.Core.Impl.Client /// <summary> /// Performs client protocol handshake. /// </summary> - private void Handshake(ClientProtocolVersion version) + private void Handshake(IgniteClientConfiguration clientConfiguration, ClientProtocolVersion version) { + bool auth = version.CompareTo(Ver110) >= 0 && clientConfiguration.Username != null; + // Send request. int messageLen; var buf = WriteMessage(stream => @@ -242,10 +278,19 @@ namespace Apache.Ignite.Core.Impl.Client // Client type: platform. stream.WriteByte(ClientType); - }, 12, out messageLen); - Debug.Assert(messageLen == 12); + // Authentication data. + if (auth) + { + var writer = BinaryUtils.Marshaller.StartMarshal(stream); + + writer.WriteString(clientConfiguration.Username); + writer.WriteString(clientConfiguration.Password); + BinaryUtils.Marshaller.FinishMarshal(writer); + } + }, 12, out messageLen); + _stream.Write(buf, 0, messageLen); // Decode response. @@ -253,6 +298,7 @@ namespace Apache.Ignite.Core.Impl.Client using (var stream = new BinaryHeapStream(res)) { + // Read input. var success = stream.ReadBool(); if (success) @@ -265,9 +311,32 @@ namespace Apache.Ignite.Core.Impl.Client var errMsg = BinaryUtils.Marshaller.Unmarshal<string>(stream); - throw new IgniteClientException(string.Format( - "Client handshake failed: '{0}'. Client version: {1}. Server version: {2}", - errMsg, version, serverVersion)); + ClientStatusCode errCode = ClientStatusCode.Fail; + + if (stream.Remaining > 0) + { + errCode = (ClientStatusCode) stream.ReadInt(); + } + + // Authentication error is handled immediately. + if (errCode == ClientStatusCode.AuthenticationFailed) + { + throw new IgniteClientException(errMsg, null, ClientStatusCode.AuthenticationFailed); + } + + // Re-try if possible. + bool retry = serverVersion.CompareTo(version) < 0 && serverVersion.Equals(Ver100); + + if (retry) + { + Handshake(clientConfiguration, serverVersion); + } + else + { + throw new IgniteClientException(string.Format( + "Client handshake failed: '{0}'. Client version: {1}. Server version: {2}", + errMsg, version, serverVersion), null, errCode); + } } }