diff --git a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs index 71ce9e7a333..30bfe763c9b 100644 --- a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs +++ b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs @@ -54,10 +54,12 @@ protected ZooKeeper CreateClientWithAddress(string address) return new ZooKeeper(address, new TimeSpan(0, 0, 0, 10000), watcher); } - protected virtual ZooKeeper CreateClientWithSasl(ISaslClient saslClient) + protected virtual ZooKeeper CreateClientWithSasl(IWatcher watcher, ISaslClient saslClient) { - CountdownWatcher watcher = new CountdownWatcher(); - return new ZooKeeper("127.0.0.1:2181", new TimeSpan(0, 0, 0, 10000), watcher, saslClient); + // "Only" use a 10s session timeout as failed tests can + // otherwise spin for a very long time in the client's + // Dispose method. + return new ZooKeeper("127.0.0.1:2181", new TimeSpan(0, 0, 0, 10), watcher, saslClient); } public class CountdownWatcher : IWatcher diff --git a/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs index 930b2441ca6..16f56286db2 100755 --- a/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs +++ b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs @@ -24,8 +24,9 @@ namespace ZooKeeperNet.Tests using S22.Sasl; using System.Net; using System.Collections.Generic; + using System.Threading; - class S22SaslClient : ISaslClient + internal class S22SaslClient : ISaslClient { // The following must be configured in zoo.conf: // @@ -45,8 +46,8 @@ class S22SaslClient : ISaslClient // // See https://cwiki.apache.org/confluence/display/ZOOKEEPER/Client-Server+mutual+authentication#Client-Servermutualauthentication-ServerConfiguration // for additional details. - private const string Username = "bob"; - private const string Password = "bobsecret"; + public string Username { get; set; } = "bob"; + public string Password { get; set; } = "bobsecret"; private SaslMechanism m = null; @@ -89,24 +90,72 @@ public void Finish() } } - [TestFixture] + internal class StateWatcher : IWatcher + { + private EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset); + + private KeeperState waitFor; + private IList observed; + + public StateWatcher(KeeperState waitFor, bool collectObserved = false) + { + this.waitFor = waitFor; + this.observed = collectObserved ? new List() : null; + } + + public void Process(WatchedEvent @event) + { + if (this.observed != null) + { + this.observed.Add(@event.State); + } + + if (@event.State == waitFor) + { + ewh.Set(); + } + } + + public bool WaitSignaled(int ms) + { + return ewh.WaitOne(ms); + } + + public IList Observed + { + get + { + return observed; + } + } + } + + [TestFixture, Explicit] public class SaslTests : AbstractZooKeeperTests { + private static readonly int maxWaitMs = 250; + [Test] public void testSasl() { string name = "/" + Guid.NewGuid() + "sasltest"; - using (var zk = CreateClientWithSasl(new S22SaslClient())) + var authWatcher = new StateWatcher(KeeperState.SaslAuthenticated); + using (var zk = CreateClientWithSasl(authWatcher, new S22SaslClient())) { + Assert.IsTrue(authWatcher.WaitSignaled(maxWaitMs)); + List acl = new List(); acl.Add(new ACL(Perms.ALL, new ZKId("sasl", "bob"))); Assert.AreEqual(name, zk.Create(name, new byte[0], acl, CreateMode.Persistent)); } - using (var zk = CreateClient()) + var connWatcher = new StateWatcher(KeeperState.SyncConnected); + using (var zk = CreateClient(connWatcher)) { + Assert.IsTrue(connWatcher.WaitSignaled(maxWaitMs)); + try { zk.GetData(name, false, new Stat()); @@ -118,9 +167,33 @@ public void testSasl() } } - using (var zk = CreateClientWithSasl(new S22SaslClient())) + var authWatcher2 = new StateWatcher(KeeperState.SaslAuthenticated); + using (var zk = CreateClientWithSasl(authWatcher2, new S22SaslClient())) { + Assert.IsTrue(authWatcher2.WaitSignaled(maxWaitMs)); + zk.GetData(name, false, new Stat()); + zk.Delete(name, -1); + } + } + + [Test] + public void testSaslBadCredentials() + { + var saslClient = new S22SaslClient() + { + Password = "evewashere" + }; + + var watcher = new StateWatcher(KeeperState.AuthFailed, true); + using (var zk = CreateClientWithSasl(watcher, saslClient)) + { + Assert.IsTrue(watcher.WaitSignaled(maxWaitMs)); + Assert.AreEqual(ZooKeeper.States.AUTH_FAILED, zk.State); + Assert.IsFalse(zk.State.IsAlive()); + + // We must have been connected before failing authentication. + Assert.IsTrue(watcher.Observed.Contains(KeeperState.SyncConnected)); } } } diff --git a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs index 255c079a204..64361a06270 100644 --- a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs +++ b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs @@ -20,6 +20,7 @@ public class ClientConnectionRequestProducer : IStartable, IDisposable { private static readonly ILog LOG = LogManager.GetLogger(typeof(ClientConnectionRequestProducer)); private const string RETRY_CONN_MSG = ", closing socket connection and attempting reconnect"; + private const string AUTH_FAILED_MSG = ", closing socket connection and setting AUTH_FAILED state"; private readonly ClientConnection conn; private readonly ZooKeeper zooKeeper; @@ -186,7 +187,13 @@ private void SendRequests() else { // this is ugly, you have a better way speak up - if (e is KeeperException.SessionExpiredException) + if (e is KeeperException.AuthFailedException) + { + LOG.InfoFormat("{0}{1}", e.Message, AUTH_FAILED_MSG); + // AUTH_FAILED state, causing IsAlive() => false. + zooKeeper.State = ZooKeeper.States.AUTH_FAILED; + } + else if (e is KeeperException.SessionExpiredException) LOG.InfoFormat("{0}, closing socket connection", e.Message); else if (e is SessionTimeoutException) LOG.InfoFormat("{0}{1}", e.Message, RETRY_CONN_MSG); @@ -203,6 +210,10 @@ private void SendRequests() { conn.consumer.QueueEvent(new WatchedEvent(KeeperState.Disconnected, EventType.None, null)); } + else if(zooKeeper.State.IsAuthFailed()) + { + conn.consumer.QueueEvent(new WatchedEvent(KeeperState.AuthFailed, EventType.None, null)); + } } } } @@ -460,8 +471,8 @@ private void PrimeConnection() try { bool lastPacket = false; - - while (true) + + do { RequestHeader h = new RequestHeader(); ReplyHeader r = new ReplyHeader(); @@ -505,12 +516,20 @@ private void PrimeConnection() } } } + // We must have received a 'ConnectResponse' by + // now, as we have waited for 1+ packets. + while (zooKeeper.State.IsConnected()); } finally { conn.saslClient.Finish(); } } + + if (zooKeeper.State.IsConnected()) + { + conn.consumer.QueueEvent(new WatchedEvent(KeeperState.SaslAuthenticated, EventType.None, null)); + } } bool hasOutgoingRequests; diff --git a/src/dotnet/ZooKeeperNet/KeeperState.cs b/src/dotnet/ZooKeeperNet/KeeperState.cs index 2e6c59bf731..dadd930e692 100644 --- a/src/dotnet/ZooKeeperNet/KeeperState.cs +++ b/src/dotnet/ZooKeeperNet/KeeperState.cs @@ -23,6 +23,8 @@ public enum KeeperState Disconnected = 0, NoSyncConnected = 1, SyncConnected = 3, + AuthFailed = 4, + SaslAuthenticated = 6, Expired = -112 } } diff --git a/src/dotnet/ZooKeeperNet/ZooKeeper.cs b/src/dotnet/ZooKeeperNet/ZooKeeper.cs index ddb6766fdc7..63834b31044 100644 --- a/src/dotnet/ZooKeeperNet/ZooKeeper.cs +++ b/src/dotnet/ZooKeeperNet/ZooKeeper.cs @@ -198,6 +198,16 @@ public bool IsAlive() return this != CLOSED && this != AUTH_FAILED; } + public bool IsConnected() + { + return this == CONNECTED; // || this == CONNECTEDREADONLY + } + + public bool IsAuthFailed() + { + return this == AUTH_FAILED; + } + public bool Equals(States other) { if (ReferenceEquals(null, other)) return false;