You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@mina.apache.org by Ma...@wellsfargo.com on 2014/08/22 18:41:03 UTC

[PATCH] Push UserInteractive keyboard-interactive support down to the session level by providing addUserInteractive method on ClientSession and making use of UserInteractive type identity in UserAuthKeyboardInteractive.

See below for message originally sent to user list...

After thinking on this some more I felt like the natural approach to my
problem was to allow for per-session UserInteractive implementations. This
would allow for a single SshClient instance with a single thread pool for
NIO, but still provide per-session user interaction for non-standard
prompts. So, I have created the following patch against 0.12.1-SNAPSHOT. The
meat of the change is to add the 'addUserInteraction' method to
ClientSession and subsequently use the corresponding identity when it is
passed into UserAuthKeyboardInteractive#init(). When process() is called,
and the attempt is made to use a UserInteractive, it will use the session
instance if present, or use the one provided by
session.getFactoryManager().getUserInteraction(). Right now, it only
supports one UserInteraction per-session, which is contrary to the existing
'idenities' logic, but seemed the most logical to avoid changing/breaking
the password iteration logic already in UserAuthKeyboardInteractive.

I'm pretty new to git so please excuse me if this not the ideal format.
Also, sorry for the extra line-breaks and such I didn't have my eclipse
formatting options quite right when I did this. I will clean it up if
necessary.

Any feedback is greatly appreciated. 

Thanks
-matt

diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index bc4971d..de3de16 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -25,6 +25,7 @@
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.client.SftpClient;
+import org.apache.sshd.client.UserInteraction;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ChannelShell;
@@ -64,6 +65,7 @@
 
     void addPasswordIdentity(String password);
     void addPublicKeyIdentity(KeyPair key);
+    void addUserInteraction(UserInteraction ui);
 
     AuthFuture auth() throws IOException;
 
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthKeyboardIntera
ctive.java
b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthKeyboardIntera
ctive.java
index 615ff42..b177922 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthKeyboardIntera
ctive.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuthKeyboardIntera
ctive.java
@@ -18,6 +18,9 @@
  */
 package org.apache.sshd.client.auth;
 
+import static
org.apache.sshd.common.SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;
+import static
org.apache.sshd.common.SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;
+
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -26,7 +29,6 @@
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.UserAuth;
 import org.apache.sshd.client.UserInteraction;
-import org.apache.sshd.client.session.ClientUserAuthServiceNew;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.session.AbstractSession;
@@ -34,12 +36,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static
org.apache.sshd.common.SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;
-import static
org.apache.sshd.common.SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;
-
 /**
  * TODO Add javadoc
- *
+ * 
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD
Project</a>
  */
 public class UserAuthKeyboardInteractive implements UserAuth {
@@ -48,26 +47,41 @@
         public String getName() {
             return "keyboard-interactive";
         }
+
         public UserAuth create() {
             return new UserAuthKeyboardInteractive();
         }
     }
 
     protected final Logger log = LoggerFactory.getLogger(getClass());
+
     private ClientSession session;
+
     private String service;
+
     private Iterator<String> passwords;
+
     private String current;
+
+    private UserInteraction userInteraction;
+
     private int nbTrials;
+
     private int maxTrials;
 
-    public void init(ClientSession session, String service, List<Object>
identities) throws Exception {
+    public void init(ClientSession session, String service, List<Object>
identities)
+                    throws Exception {
         this.session = session;
         this.service = service;
         List<String> pwds = new ArrayList<String>();
         for (Object o : identities) {
             if (o instanceof String) {
                 pwds.add((String) o);
+            } else if (o instanceof UserInteraction) {
+                if (this.userInteraction != null)
+                    throw new IllegalArgumentException(
+                                    "Only one UserInteraction is supported
in a given session");
+                this.userInteraction = (UserInteraction) o;
             }
         }
         this.passwords = pwds.iterator();
@@ -99,7 +113,7 @@
             String name = buffer.getString();
             String instruction = buffer.getString();
             String language_tag = buffer.getString();
-            log.info("Received {} {} {}", new Object[]{name, instruction,
language_tag});
+            log.info("Received {} {} {}", new Object[] { name, instruction,
language_tag });
             int num = buffer.getInt();
             String[] prompt = new String[num];
             boolean[] echo = new boolean[num];
@@ -113,12 +127,16 @@
             String[] rep = null;
             if (num == 0) {
                 rep = new String[0];
-            } else if (num == 1 && current != null && !echo[0] &&
prompt[0].toLowerCase().startsWith("password:")) {
+            } else if (num == 1 && current != null && !echo[0]
+                            &&
prompt[0].toLowerCase().startsWith("password:")) {
                 rep = new String[] { current };
             } else {
-                UserInteraction ui =
session.getFactoryManager().getUserInteraction();
+                UserInteraction ui = getUserInteraction();
                 if (ui != null) {
-                    String dest = session.getUsername() + "@" +
((AbstractSession) session).getIoSession().getRemoteAddress().toString();
+                    String dest = session.getUsername()
+                                    + "@"
+                                    + ((AbstractSession)
session).getIoSession().getRemoteAddress()
+                                                    .toString();
                     rep = ui.interactive(dest, name, instruction, prompt,
echo);
                 }
             }
@@ -137,6 +155,11 @@
         throw new IllegalStateException("Received unknown packet");
     }
 
+    private UserInteraction getUserInteraction() {
+        return (userInteraction != null) ? userInteraction :
session.getFactoryManager()
+                        .getUserInteraction();
+    }
+
     public void destroy() {
     }
 }
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.j
ava
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.j
ava
index 7ed3cd3..658bf84 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.j
ava
+++
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.j
ava
@@ -32,6 +32,7 @@
 import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.client.ServerKeyVerifier;
 import org.apache.sshd.client.SftpClient;
+import org.apache.sshd.client.UserInteraction;
 import org.apache.sshd.client.auth.deprecated.UserAuth;
 import org.apache.sshd.client.auth.deprecated.UserAuthAgent;
 import org.apache.sshd.client.auth.deprecated.UserAuthKeyboardInteractive;
@@ -127,6 +128,10 @@
     public void addPublicKeyIdentity(KeyPair key) {
         identities.add(key);
     }
+    
+    public void addUserInteraction(UserInteraction ui) {
+        identities.add(ui);
+    }
 
     public AuthFuture auth() throws IOException {
         if (username == null) {
diff --git a/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
b/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
index b1dfa41..b5f1cfa 100644
--- a/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/ClientTest.java
@@ -86,19 +86,23 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 /**
  * TODO Add javadoc
- *
+ * 
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD
Project</a>
  */
 public class ClientTest extends BaseTest {
 
     private SshServer sshd;
+
     private int port;
+
     private CountDownLatch authLatch;
+
     private CountDownLatch channelLatch;
 
     @Before
@@ -118,44 +122,42 @@
         });
         sshd.setPasswordAuthenticator(new BogusPasswordAuthenticator());
         sshd.setPublickeyAuthenticator(new BogusPublickeyAuthenticator());
-        sshd.setServiceFactories(Arrays.asList(
-                new ServerUserAuthService.Factory() {
+        sshd.setServiceFactories(Arrays.asList(new
ServerUserAuthService.Factory() {
+            @Override
+            public Service create(Session session) throws IOException {
+                return new ServerUserAuthService(session) {
                     @Override
-                    public Service create(Session session) throws
IOException {
-                        return new ServerUserAuthService(session) {
-                            @Override
-                            public void process(byte cmd, Buffer buffer)
throws Exception {
-                                authLatch.await();
-                                super.process(cmd, buffer);
-                            }
-                        };
+                    public void process(byte cmd, Buffer buffer) throws
Exception {
+                        authLatch.await();
+                        super.process(cmd, buffer);
                     }
-                },
-                new ServerConnectionService.Factory()
-        ));
-        sshd.setChannelFactories(Arrays.<NamedFactory<Channel>>asList(
-                new ChannelSession.Factory() {
-                    @Override
-                    public Channel create() {
-                        return new ChannelSession() {
+                };
+            }
+        }, new ServerConnectionService.Factory()));
+        sshd.setChannelFactories(Arrays.<NamedFactory<Channel>> asList(
+                        new ChannelSession.Factory() {
                             @Override
-                            public OpenFuture open(int recipient, int
rwsize, int rmpsize, Buffer buffer) {
-                                try {
-                                    channelLatch.await();
-                                } catch (InterruptedException e) {
-                                    throw new RuntimeSshException(e);
-                                }
-                                return super.open(recipient, rwsize,
rmpsize, buffer);
-                            }
+                            public Channel create() {
+                                return new ChannelSession() {
+                                    @Override
+                                    public OpenFuture open(int recipient,
int rwsize, int rmpsize,
+                                                    Buffer buffer) {
+                                        try {
+                                            channelLatch.await();
+                                        } catch (InterruptedException e) {
+                                            throw new
RuntimeSshException(e);
+                                        }
+                                        return super.open(recipient,
rwsize, rmpsize, buffer);
+                                    }
 
-                            @Override
-                            public String toString() {
-                                return "ChannelSession" + "[id=" + id + ",
recipient=" + recipient + "]";
+                                    @Override
+                                    public String toString() {
+                                        return "ChannelSession" + "[id=" +
id + ", recipient="
+                                                        + recipient + "]";
+                                    }
+                                };
                             }
-                        };
-                    }
-                },
-                new TcpipServerChannel.DirectTcpipFactory()));
+                        }, new TcpipServerChannel.DirectTcpipFactory()));
         sshd.start();
     }
 
@@ -182,7 +184,6 @@
         channel.setStreaming(ClientChannel.Streaming.Async);
         channel.open().verify();
 
-
         final byte[] message = "0123456789\n".getBytes();
         final int nbMessages = 1000;
 
@@ -191,62 +192,62 @@
         final AtomicInteger writes = new AtomicInteger(nbMessages);
 
         channel.getAsyncIn().write(new Buffer(message))
-                .addListener(new SshFutureListener<IoWriteFuture>() {
-                    public void operationComplete(IoWriteFuture future) {
-                        try {
-                            if (future.isWritten()) {
-                                if (writes.decrementAndGet() > 0) {
-                                    channel.getAsyncIn().write(new
Buffer(message)).addListener(this);
-                                } else {
-                                    channel.getAsyncIn().close(false);
+                        .addListener(new SshFutureListener<IoWriteFuture>()
{
+                            public void operationComplete(IoWriteFuture
future) {
+                                try {
+                                    if (future.isWritten()) {
+                                        if (writes.decrementAndGet() > 0) {
+                                            channel.getAsyncIn().write(new
Buffer(message))
+
.addListener(this);
+                                        } else {
+
channel.getAsyncIn().close(false);
+                                        }
+                                    } else {
+                                        throw new SshException("Error
writing", future
+                                                        .getException());
+                                    }
+                                } catch (IOException e) {
+                                    if (!channel.isClosing()) {
+                                        e.printStackTrace();
+                                        channel.close(true);
+                                    }
                                 }
-                            } else {
-                                throw new SshException("Error writing",
future.getException());
                             }
-                        } catch (IOException e) {
-                            if (!channel.isClosing()) {
-                                e.printStackTrace();
-                                channel.close(true);
-                            }
-                        }
+                        });
+        channel.getAsyncOut().read(new Buffer()).addListener(new
SshFutureListener<IoReadFuture>() {
+            public void operationComplete(IoReadFuture future) {
+                try {
+                    future.verify();
+                    Buffer buffer = future.getBuffer();
+                    baosOut.write(buffer.array(), buffer.rpos(),
buffer.available());
+                    buffer.rpos(buffer.rpos() + buffer.available());
+                    buffer.compact();
+                    channel.getAsyncOut().read(buffer).addListener(this);
+                } catch (IOException e) {
+                    if (!channel.isClosing()) {
+                        e.printStackTrace();
+                        channel.close(true);
                     }
-                });
-        channel.getAsyncOut().read(new Buffer())
-                .addListener(new SshFutureListener<IoReadFuture>() {
-                    public void operationComplete(IoReadFuture future) {
-                        try {
-                            future.verify();
-                            Buffer buffer = future.getBuffer();
-                            baosOut.write(buffer.array(), buffer.rpos(),
buffer.available());
-                            buffer.rpos(buffer.rpos() +
buffer.available());
-                            buffer.compact();
-
channel.getAsyncOut().read(buffer).addListener(this);
-                        } catch (IOException e) {
-                            if (!channel.isClosing()) {
-                                e.printStackTrace();
-                                channel.close(true);
-                            }
-                        }
+                }
+            }
+        });
+        channel.getAsyncErr().read(new Buffer()).addListener(new
SshFutureListener<IoReadFuture>() {
+            public void operationComplete(IoReadFuture future) {
+                try {
+                    future.verify();
+                    Buffer buffer = future.getBuffer();
+                    baosErr.write(buffer.array(), buffer.rpos(),
buffer.available());
+                    buffer.rpos(buffer.rpos() + buffer.available());
+                    buffer.compact();
+                    channel.getAsyncErr().read(buffer).addListener(this);
+                } catch (IOException e) {
+                    if (!channel.isClosing()) {
+                        e.printStackTrace();
+                        channel.close(true);
                     }
-                });
-        channel.getAsyncErr().read(new Buffer())
-                .addListener(new SshFutureListener<IoReadFuture>() {
-                    public void operationComplete(IoReadFuture future) {
-                        try {
-                            future.verify();
-                            Buffer buffer = future.getBuffer();
-                            baosErr.write(buffer.array(), buffer.rpos(),
buffer.available());
-                            buffer.rpos(buffer.rpos() +
buffer.available());
-                            buffer.compact();
-
channel.getAsyncErr().read(buffer).addListener(this);
-                        } catch (IOException e) {
-                            if (!channel.isClosing()) {
-                                e.printStackTrace();
-                                channel.close(true);
-                            }
-                        }
-                    }
-                });
+                }
+            }
+        });
 
         channel.waitFor(ClientChannel.CLOSED, 0);
 
@@ -272,7 +273,8 @@
         } catch (SshException e) {
             // That's ok, the channel is being closed by the other side
         }
-        assertEquals(ChannelExec.CLOSED,
channel.waitFor(ChannelExec.CLOSED, 0) & ChannelExec.CLOSED);
+        assertEquals(ChannelExec.CLOSED,
channel.waitFor(ChannelExec.CLOSED, 0)
+                        & ChannelExec.CLOSED);
         session.close(false).await();
         client.stop();
     }
@@ -384,7 +386,6 @@
         session.authPassword("smx", "smx").await().isSuccess();
         ClientChannel channel =
session.createChannel(ClientChannel.CHANNEL_SHELL);
 
-
         ByteArrayOutputStream sent = new ByteArrayOutputStream();
         PipedOutputStream pipedIn = new PipedOutputStream();
         OutputStream teeOut = new TeeOutputStream(sent, pipedIn);
@@ -419,10 +420,10 @@
     public void testClientWithLengthyDialog() throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
         // Reduce window size and packet size
-//        client.getProperties().put(SshClient.WINDOW_SIZE,
Integer.toString(0x20000));
-//        client.getProperties().put(SshClient.MAX_PACKET_SIZE,
Integer.toString(0x1000));
-//        sshd.getProperties().put(SshServer.WINDOW_SIZE,
Integer.toString(0x20000));
-//        sshd.getProperties().put(SshServer.MAX_PACKET_SIZE,
Integer.toString(0x1000));
+        // client.getProperties().put(SshClient.WINDOW_SIZE,
Integer.toString(0x20000));
+        // client.getProperties().put(SshClient.MAX_PACKET_SIZE,
Integer.toString(0x1000));
+        // sshd.getProperties().put(SshServer.WINDOW_SIZE,
Integer.toString(0x20000));
+        // sshd.getProperties().put(SshServer.MAX_PACKET_SIZE,
Integer.toString(0x1000));
         client.start();
         ClientSession session = client.connect("localhost",
port).await().getSession();
         session.authPassword("smx", "smx");
@@ -464,7 +465,7 @@
         client.stop();
 
         assertTrue(BufferUtils.equals(sent.toByteArray(),
out.toByteArray()));
-        //assertArrayEquals(sent.toByteArray(), out.toByteArray());
+        // assertArrayEquals(sent.toByteArray(), out.toByteArray());
     }
 
     @Test(expected = SshException.class)
@@ -552,10 +553,12 @@
     @Test
     public void testPublicKeyAuthNew() throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthPublicKey.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthPublicKey.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
-
session.addPublicKeyIdentity(Utils.createTestHostKeyProvider().loadKey(KeyPa
irProvider.SSH_RSA));
+
session.addPublicKeyIdentity(Utils.createTestHostKeyProvider().loadKey(
+                        KeyPairProvider.SSH_RSA));
         session.auth().verify();
     }
 
@@ -568,10 +571,12 @@
             }
         });
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthPublicKey.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthPublicKey.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
-        session.addPublicKeyIdentity(new
SimpleGeneratorHostKeyProvider(null,
"RSA").loadKey(KeyPairProvider.SSH_RSA));
+        session.addPublicKeyIdentity(new
SimpleGeneratorHostKeyProvider(null, "RSA")
+                        .loadKey(KeyPairProvider.SSH_RSA));
         session.addPublicKeyIdentity(pair);
         session.auth().verify();
     }
@@ -579,7 +584,8 @@
     @Test
     public void testPasswordAuthNew() throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthPassword.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthPassword.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
         session.addPasswordIdentity("smx");
@@ -589,7 +595,8 @@
     @Test
     public void testPasswordAuthNewWithFailureOnFirstIdentity() throws
Exception {
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthPassword.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthPassword.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
         session.addPasswordIdentity("bad");
@@ -600,7 +607,8 @@
     @Test
     public void testKeyboardInteractiveAuthNew() throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
         session.addPasswordIdentity("smx");
@@ -610,7 +618,8 @@
     @Test
     public void testKeyboardInteractiveAuthNewWithFailureOnFirstIdentity()
throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
         client.start();
         ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
         session.addPasswordIdentity("bad");
@@ -623,11 +632,14 @@
         final AtomicInteger count = new AtomicInteger();
         SshClient client = SshClient.setUpDefaultClient();
         client.getProperties().put(ClientFactoryManager.PASSWORD_PROMPTS,
"3");
-
client.setUserAuthFactories(Arrays.<NamedFactory<UserAuth>>asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
         client.setUserInteraction(new UserInteraction() {
             public void welcome(String banner) {
             }
-            public String[] interactive(String destination, String name,
String instruction, String[] prompt, boolean[] echo) {
+
+            public String[] interactive(String destination, String name,
String instruction,
+                            String[] prompt, boolean[] echo) {
                 count.incrementAndGet();
                 return new String[] { "bad" };
             }
@@ -639,12 +651,94 @@
         assertTrue(future.isFailure());
         assertEquals(3, count.get());
     }
+    
+    @Test
+    public void testKeyboardInteractiveInSessionUserInteractive() throws
Exception {
+        final AtomicInteger count = new AtomicInteger();
+        SshClient client = SshClient.setUpDefaultClient();
+        client.getProperties().put(ClientFactoryManager.PASSWORD_PROMPTS,
"3");
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.start();
+        ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
+        session.addUserInteraction(new UserInteraction() {
+            public void welcome(String banner) {
+            }
+
+            public String[] interactive(String destination, String name,
String instruction,
+                            String[] prompt, boolean[] echo) {
+                count.incrementAndGet();
+                return new String[] { "smx" };
+            }
+        });
+        AuthFuture future = session.auth();
+        future.await();
+        assertTrue(future.isSuccess());
+        assertFalse(future.isFailure());
+        assertEquals(1, count.get());
+    }
+
+    @Test
+    public void testKeyboardInteractiveInSessionUserInteractiveFailure()
throws Exception {
+        final AtomicInteger count = new AtomicInteger();
+        SshClient client = SshClient.setUpDefaultClient();
+        client.getProperties().put(ClientFactoryManager.PASSWORD_PROMPTS,
"3");
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.start();
+        ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
+        session.addUserInteraction(new UserInteraction() {
+            public void welcome(String banner) {
+            }
+
+            public String[] interactive(String destination, String name,
String instruction,
+                            String[] prompt, boolean[] echo) {
+                count.incrementAndGet();
+                return new String[] { "bad" };
+            }
+        });
+        AuthFuture future = session.auth();
+        future.await();
+        assertTrue(future.isFailure());
+        assertEquals(3, count.get());
+    }
+
+    @Test
+    public void testKeyboardInteractiveMultipleInSessionNotSupported()
throws Exception {
+        final AtomicInteger count = new AtomicInteger();
+        SshClient client = SshClient.setUpDefaultClient();
+        client.getProperties().put(ClientFactoryManager.PASSWORD_PROMPTS,
"3");
+        client.setUserAuthFactories(Arrays
+                        .<NamedFactory<UserAuth>> asList(new
UserAuthKeyboardInteractive.Factory()));
+        client.start();
+        ClientSession session = client.connect("smx", "localhost",
port).await().getSession();
+        session.addUserInteraction(new UserInteraction() {
+            public void welcome(String banner) {
+            }
+
+            public String[] interactive(String destination, String name,
String instruction,
+                            String[] prompt, boolean[] echo) {
+                return new String[] { "bad" };
+            }
+        });
+        session.addUserInteraction(new UserInteraction() {
+            public void welcome(String banner) {
+            }
+
+            public String[] interactive(String destination, String name,
String instruction,
+                            String[] prompt, boolean[] echo) {
+                return new String[] { "smx" };
+            }
+        });
+        AuthFuture future = session.auth();
+        future.await();
+        assertFalse(future.isSuccess());
+    }
 
     @Test
     public void testClientDisconnect() throws Exception {
         TestEchoShellFactory.TestEchoShell.latch = new CountDownLatch(1);
-        try
-        {
+        try {
             SshClient client = SshClient.setUpDefaultClient();
             client.start();
             ClientSession session = client.connect("localhost",
port).await().getSession();
@@ -658,7 +752,8 @@
             channel.setErr(err);
             channel.open().await();
 
-//            ((AbstractSession)
session).disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, "Cancel");
+            // ((AbstractSession)
session).disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION,
+            // "Cancel");
             AbstractSession cs = (AbstractSession) session;
             Buffer buffer =
cs.createBuffer(SshConstants.SSH_MSG_DISCONNECT);
             buffer.putInt(SshConstants.SSH2_DISCONNECT_BY_APPLICATION);
@@ -678,19 +773,14 @@
     public void testWaitAuth() throws Exception {
         SshClient client = SshClient.setUpDefaultClient();
         final AtomicBoolean ok = new AtomicBoolean();
-        client.setServerKeyVerifier(
-                new ServerKeyVerifier() {
-                    public boolean verifyServerKey(
-                            ClientSession sshClientSession,
-                            SocketAddress remoteAddress,
-                            PublicKey serverKey
-                    ) {
-                        System.out.println(serverKey);
-                        ok.set(true);
-                        return true;
-                    }
-                }
-        );
+        client.setServerKeyVerifier(new ServerKeyVerifier() {
+            public boolean verifyServerKey(ClientSession sshClientSession,
+                            SocketAddress remoteAddress, PublicKey
serverKey) {
+                System.out.println(serverKey);
+                ok.set(true);
+                return true;
+            }
+        });
         client.start();
         ClientSession session = client.connect("localhost",
port).await().getSession();
         session.waitFor(ClientSession.WAIT_AUTH, 10000);
@@ -711,6 +801,7 @@
         public Command create() {
             return new TestEchoShell();
         }
+
         public static class TestEchoShell extends EchoShell {
 
             public static CountDownLatch latch = new CountDownLatch(1);

-----Original Message-----
From: Matthew.W.Pitts@wellsfargo.com [mailto:Matthew.W.Pitts@wellsfargo.com]

Sent: Thursday, August 21, 2014 10:52 PM
To: users@mina.apache.org
Subject: SSHD SshClient - should I reuse the same instance for multiple
independent sessions?

Hey all, thanks for all the work that has gone into Mina/SSHD – great
libraries!

I have a codebase that is currently running quite well with SSHD v0.8.0, but
I am looking to upgrade to 0.12.0 for some of the fixes/improvements that
have come out since 0.8.0. For an overview of how I’m using SSHD - the
system executes a 10-50 SSH commands – each in its own channel - to 2000 or
so (and growing) devices every day. Some of the commands/channels will
re-use an existing session by way of a keyed pooling system I have setup for
the sessions. This all works quite well right now.

The current model uses a single SshClient instance and spawns ALL sessions
to each respective host from that same instance. This is true regardless of
the details of each session (username, destination host, port,
authentication, etc). This obviously avoids the need to call
SshClient.setupDefaultClient() for each and every SSH session. I’m not sure
if this is the recommended way, but again, it is working now.

I am prototyping my code with 0.12.0 and refactoring some things to align
with how I see the differences in the versions and I’ve run into a bit of
conundrum. I want to take advantage of the keyboard-interactive support,
which appears to be done by calling SshClient.setUserInteraction with an
appropriate implementation. The problem is that with my shared-SshClient
model it is not practical to give it a single UserInteraction implementation
to support all subsequent sessions since the credentials aren’t known ‘ahead
of time’ when the global SshClient is created. So, as part of my prototyping
I have refactored my model to use an SshClient instance per session, thereby
allowing me to provide a UserInteraction impl that is appropriate for each
particular session. In my testing this seems to be work, but again, I’m not
sure if this is the recommended approach.

So my question is: when using SSHD for relatively short-lived sessions (a
few minutes at a time) that are spawned in lots of different threads to
different host+credential combinations (password, private key, etc.); is it
appropriate for performance/scalability reasons to use a single SshClient
instance to spawn each session? If this is true, then is there a
suggested/recommended approach for dealing with keyboard-interactive using
different credentials for each session from a single UserInteraction
instance?

OR - Is the creation of SshClient instances pretty inexpensive so it would
then be OK to create a new SshdClient instance for each session where one
can then set the UserInteraction impl appropriately? If this is true, what
would be a good setting for the number of NIO threads to use for each
SshClient instance in a system like this? The default, which AFAIK is CPU
cores + 1, is a bit excessive I think for a system like mine that could be
creating a few thousand sessions at any given time - that could mean a lot
of threads on a 8 or 16 core system. Since each session instance is borrowed
from a pool, used exclusively and then returned, I figured just one thread
would be fine, but I welcome suggestions.

OR - Would it be more appropriate for the UserInteraction impl be set on the
ClientSession rather than the SshClient? To me, this would seem to align
more with the other ‘auth’ methods that are already in the ClientSession API
(e.g. addPasswordIdentity and addPublicKeyIdentity)? This would then allow
for a shared SshClient, but with customizable keyboard-interaction per
session.

If this is confusing, let me know and I’ll try to restate appropriately.

Any advice on this is greatly appreciated.

Thanks,

Matthew Pitts
 
Developer
Security Solutions Design & Automation
 
Wells Fargo Bank | Tel 336.608.3332 | Cell 336.202.3913 | Winston-Salem, NC
| MAC D4002-040
matthew.w.pitts@wellsfargo.com