diff --git a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs index 55c82ef6867..71ce9e7a333 100644 --- a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs +++ b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs @@ -54,6 +54,12 @@ protected ZooKeeper CreateClientWithAddress(string address) return new ZooKeeper(address, new TimeSpan(0, 0, 0, 10000), watcher); } + protected virtual ZooKeeper CreateClientWithSasl(ISaslClient saslClient) + { + CountdownWatcher watcher = new CountdownWatcher(); + return new ZooKeeper("127.0.0.1:2181", new TimeSpan(0, 0, 0, 10000), watcher, saslClient); + } + public class CountdownWatcher : IWatcher { readonly ManualResetEvent resetEvent = new ManualResetEvent(false); diff --git a/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs new file mode 100755 index 00000000000..930b2441ca6 --- /dev/null +++ b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs @@ -0,0 +1,127 @@ +/* + * 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 ZooKeeperNet.Tests +{ + using System; + using NUnit.Framework; + using Org.Apache.Zookeeper.Data; + using ZooKeeperNet; + using S22.Sasl; + using System.Net; + using System.Collections.Generic; + + class S22SaslClient : ISaslClient + { + // The following must be configured in zoo.conf: + // + // authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider + // + // The following in jaas.conf: + // + // Server { + // org.apache.zookeeper.server.auth.DigestLoginModule required + // user_super="adminsecret" + // user_bob="bobsecret"; + // }; + // + // And the server must be started with: + // + // -Djava.security.auth.login.config=.../jaas.conf + // + // 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"; + + private SaslMechanism m = null; + + public byte[] Start(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + m = SaslFactory.Create("DIGEST-MD5"); + + m.Properties.Add("Username", Username); + m.Properties.Add("Password", Password); + m.Properties.Add("Protocol", "zookeeper"); + + // Client start is empty. + return null; + } + + public bool IsCompleted + { + get + { + return m == null || m.IsCompleted; + } + } + + public bool HasLastPacket + { + get + { + return false; // not GSSAPI. + } + } + + public byte[] EvaluateChallenge(byte[] token) + { + return m.GetResponse(token); + } + + public void Finish() + { + m = null; + } + } + + [TestFixture] + public class SaslTests : AbstractZooKeeperTests + { + [Test] + public void testSasl() + { + string name = "/" + Guid.NewGuid() + "sasltest"; + + using (var zk = CreateClientWithSasl(new S22SaslClient())) + { + 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()) + { + try + { + zk.GetData(name, false, new Stat()); + Assert.Fail("Should have received a permission error"); + } + catch (KeeperException e) + { + Assert.AreEqual(KeeperException.Code.NOAUTH, e.ErrorCode); + } + } + + using (var zk = CreateClientWithSasl(new S22SaslClient())) + { + zk.GetData(name, false, new Stat()); + } + } + } +} diff --git a/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj b/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj index 473173b9248..4b37c271f8a 100644 --- a/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj +++ b/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj @@ -66,6 +66,9 @@ False ..\lib\pnunit.framework.dll + + ..\lib\S22.Sasl.dll + 3.5 @@ -77,6 +80,7 @@ + diff --git a/src/dotnet/ZooKeeperNet/ClientConnection.cs b/src/dotnet/ZooKeeperNet/ClientConnection.cs index b0394e423eb..54737f7f53c 100644 --- a/src/dotnet/ZooKeeperNet/ClientConnection.cs +++ b/src/dotnet/ZooKeeperNet/ClientConnection.cs @@ -107,6 +107,7 @@ public static bool DisableAutoWatchReset internal string hosts; internal readonly ZooKeeper zooKeeper; internal readonly ZKWatchManager watcher; + internal readonly ISaslClient saslClient; internal readonly List serverAddrs = new List(); internal readonly List authInfo = new List(); internal TimeSpan readTimeout; @@ -122,7 +123,7 @@ public bool IsClosed internal ClientConnectionRequestProducer producer; internal ClientConnectionEventConsumer consumer; - + /// /// Initializes a new instance of the class. /// @@ -130,8 +131,9 @@ public bool IsClosed /// The session timeout. /// The zoo keeper. /// The watch manager. - public ClientConnection(string connectionString, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher): - this(connectionString, sessionTimeout, zooKeeper, watcher, 0, new byte[16], DefaultConnectTimeout) + /// The SASL client. + public ClientConnection(string connectionString, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient) : + this(connectionString, sessionTimeout, zooKeeper, watcher, saslClient, 0, new byte[16], DefaultConnectTimeout) { } @@ -171,14 +173,47 @@ public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeep /// The session timeout. /// The zoo keeper. /// The watch manager. + /// The SASL client. + /// The session id. + /// The session passwd. + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd) + : this(hosts, sessionTimeout, zooKeeper, watcher, saslClient, 0, new byte[16], DefaultConnectTimeout) + { + } + + + /// + /// Initializes a new instance of the class. + /// + /// The hosts. + /// The session timeout. + /// The zoo keeper. + /// The watch manager. + /// The session id. + /// The session passwd. + /// Connection Timeout. + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) : + this(hosts, sessionTimeout, zooKeeper, watcher, null, sessionId, sessionPasswd, connectTimeout) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The hosts. + /// The session timeout. + /// The zoo keeper. + /// The watch manager. + /// The SASL client. /// The session id. /// The session passwd. /// Connection Timeout. - public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) { this.hosts = hosts; this.zooKeeper = zooKeeper; this.watcher = watcher; + this.saslClient = saslClient; SessionTimeout = sessionTimeout; SessionId = sessionId; SessionPassword = sessionPasswd; diff --git a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs index 4fad2eb23d9..255c079a204 100644 --- a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs +++ b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs @@ -441,6 +441,79 @@ private void PrimeConnection() { LOG.InfoFormat("Socket connection established to {0}, initiating session", client.Client.RemoteEndPoint); ConnectRequest conReq = new ConnectRequest(0, lastZxid, Convert.ToInt32(conn.SessionTimeout.TotalMilliseconds), conn.SessionId, conn.SessionPassword); + Packet conPacket = new Packet(null, null, conReq, null, null, null, null, null); + + if (conn.saslClient != null) + { + lock (outgoingQueue) + { + // SASL negociation is synchronous, and must complete before we send the (non-SASL) auth data and + // watches. We explicitly drive the send/receive as the queue processing loop is not active yet. + + // First, push the ConnectRequest down the pipe. + DoSend(conPacket); + conPacket = null; + + byte[] token = conn.saslClient.Start((IPEndPoint)client.Client.LocalEndPoint, + (IPEndPoint)client.Client.RemoteEndPoint); + + try + { + bool lastPacket = false; + + while (true) + { + RequestHeader h = new RequestHeader(); + ReplyHeader r = new ReplyHeader(); + h.Type = (int)OpCode.SASL; + GetSASLRequest request = new GetSASLRequest(token != null ? token : new byte[0]); + SetSASLResponse response = new SetSASLResponse(); + + Packet p = new Packet(h, r, request, response, null, null, null, null); + + // Push the packet. + DoSend(p); + + // Synchronously wait for the response. + if (!p.WaitUntilFinishedSlim(conn.ConnectionTimeout)) + { + throw new TimeoutException(new StringBuilder("The request ").Append(request).Append(" timed out while waiting for a response from the server.").ToString()); + } + + if (r.Err != 0) + { + throw KeeperException.Create((KeeperException.Code)Enum.ToObject(typeof(KeeperException.Code), r.Err)); + } + + if (lastPacket) + { + break; + } + + // SASL round. + token = conn.saslClient.EvaluateChallenge(response.Token); + + if (conn.saslClient.IsCompleted) + { + if (conn.saslClient.HasLastPacket) + { + lastPacket = true; + } + else + { + break; + } + } + } + } + finally + { + conn.saslClient.Finish(); + } + } + } + + bool hasOutgoingRequests; lock (outgoingQueue) { @@ -459,10 +532,19 @@ private void PrimeConnection() addPacketFirst( new Packet(new RequestHeader(-4, (int) OpCode.Auth), null, new AuthPacket(0, id.Scheme, id.GetData()), null, null, null, null, null)); - addPacketFirst(new Packet(null, null, conReq, null, null, null, null, null)); - + // The ConnectRequest packet has already been sent in the SASL case. + if (conPacket != null) + { + addPacketFirst(conPacket); + conPacket = null; + } + + hasOutgoingRequests = !outgoingQueue.IsEmpty(); } - packetAre.Set(); + + if (hasOutgoingRequests) + packetAre.Set(); + if (LOG.IsDebugEnabled) LOG.DebugFormat("Session establishment request sent on {0}",client.Client.RemoteEndPoint); } diff --git a/src/dotnet/ZooKeeperNet/ISaslClient.cs b/src/dotnet/ZooKeeperNet/ISaslClient.cs new file mode 100644 index 00000000000..7ee7548a52d --- /dev/null +++ b/src/dotnet/ZooKeeperNet/ISaslClient.cs @@ -0,0 +1,67 @@ +/* + * 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. + * + */ +using System.Net; + +namespace ZooKeeperNet +{ + public interface ISaslClient + { + /// + /// Start an authentication session. + /// + /// + /// An initial, possibly empty, initial response to send to + /// the server. + /// + byte[] Start(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint); + + /// + /// Determines whether the exchange has completed. + /// + /// Whether the exchange has completed. + bool IsCompleted { get; } + + /// + /// Determines whether authentication using this client or + /// mechanism requires the emission of a "last packet," as + /// defined by ZooKeeper: + /// + /// "GSSAPI: server sends a final packet after authentication + /// succeeds or fails." + /// "non-GSSAPI: no final packet from server." + /// + /// https://github.com/apache/zookeeper/blob/11c07921c15e/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java#L285-L293 + /// + /// Whether a "last packet" is required. + bool HasLastPacket { get; } + + /// + /// Evaluates the challenge data and generate a response. + /// + /// The challenge sent from the server. + /// The response to send to the server. + byte[] EvaluateChallenge(byte[] challenge); + + /// + /// Marks authentication as complete, allowing the client to + /// release resources which won't be needed until the next + /// . + /// + void Finish(); + } +} diff --git a/src/dotnet/ZooKeeperNet/OpCode.cs b/src/dotnet/ZooKeeperNet/OpCode.cs index f0c138b19b8..ed1ccc4cec9 100644 --- a/src/dotnet/ZooKeeperNet/OpCode.cs +++ b/src/dotnet/ZooKeeperNet/OpCode.cs @@ -33,6 +33,7 @@ public enum OpCode GetChildren2 = 12, Auth = 100, SetWatches = 101, + SASL = 102, CreateSession = -10, CloseSession = -11, Error = -1, diff --git a/src/dotnet/ZooKeeperNet/ZooKeeper.cs b/src/dotnet/ZooKeeperNet/ZooKeeper.cs index d89f437d58c..ddb6766fdc7 100644 --- a/src/dotnet/ZooKeeperNet/ZooKeeper.cs +++ b/src/dotnet/ZooKeeperNet/ZooKeeper.cs @@ -266,21 +266,35 @@ public override string ToString() /// a watcher object which will be notified of state changes, may /// also be notified for node events /// - public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher) + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher) : + this(connectstring, sessionTimeout, watcher, null) + { + } + + /// + /// An optional object implementing the interface which will be used by the + /// to authenticate with the server immediately after (re)connect. + /// + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, ISaslClient saslClient) { LOG.InfoFormat("Initiating client connection, connectstring={0} sessionTimeout={1} watcher={2}", connectstring, sessionTimeout, watcher); watchManager.DefaultWatcher = watcher; - cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager); + cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, saslClient); cnxn.Start(); } - public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, long sessionId, byte[] sessionPasswd) + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, long sessionId, byte[] sessionPasswd) : + this(connectstring, sessionTimeout, watcher, null, sessionId, sessionPasswd) + { + } + + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd) { LOG.InfoFormat("Initiating client connection, connectstring={0} sessionTimeout={1} watcher={2} sessionId={3} sessionPasswd={4}", connectstring, sessionTimeout, watcher, sessionId, (sessionPasswd == null ? "" : "")); watchManager.DefaultWatcher = watcher; - cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, sessionId, sessionPasswd); + cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, saslClient, sessionId, sessionPasswd); cnxn.Start(); } diff --git a/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj b/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj index eb4a018b828..065e92f24c8 100644 --- a/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj +++ b/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj @@ -145,6 +145,7 @@ + @@ -197,11 +198,11 @@ - \ No newline at end of file diff --git a/src/dotnet/lib/S22.Sasl.dll b/src/dotnet/lib/S22.Sasl.dll new file mode 100755 index 00000000000..c1107550cf4 Binary files /dev/null and b/src/dotnet/lib/S22.Sasl.dll differ