You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/04/17 12:33:05 UTC

[1/6] mina-sshd git commit: [SSHD-816] Added mechanism for specifying non-default SftpClientFactory for SFTP CLI code

Repository: mina-sshd
Updated Branches:
  refs/heads/master 76fe82188 -> 04c579408


[SSHD-816] Added mechanism for specifying non-default SftpClientFactory for SFTP CLI code


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/04c57940
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/04c57940
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/04c57940

Branch: refs/heads/master
Commit: 04c57940851f2a13347ea3c96eb4217ca8c13ca1
Parents: 2b7e22a
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Tue Apr 17 13:10:08 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Tue Apr 17 15:32:31 2018 +0300

----------------------------------------------------------------------
 README.md                                       | 16 ++++++-
 .../apache/sshd/cli/client/SftpCommandMain.java | 49 +++++++++++++++++++-
 .../sshd/cli/server/SshServerCliSupport.java    |  2 +-
 3 files changed, 63 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/04c57940/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index bb4f586..4878117 100644
--- a/README.md
+++ b/README.md
@@ -1293,6 +1293,18 @@ In order to use this CLI code as part of another project, one needs to include t
     </dependency>
 ```
 
+### Command line clients
+
+* **SftpCommandMain** - by default uses an internal `SftpClientFactory`. This can be overridden as follows:
+
+1. Provide a `-o SftpClientFactory=XXX` command line argument where the option specifies the fully-qualified name of
+the class that implements this interface.
+
+2. Add a `META-INF\services\org.apache.sshd.client.subsystem.sftp.SftpClientFactory` file containing the fully-qualified name of
+the class that implements this interface. **Note:** if more than one such instance is detected an exception is thrown.
+
+**Note:** The specified class(es) must be public and contain a public no-args constructor.
+
 ### Command line SSH daemon
 
 * **Port** - by default the SSH server sets up to list on port 8000 in order to avoid conflicts with any running SSH O/S daemon. This can be modified by providing a `-p NNNN`
@@ -1302,8 +1314,8 @@ or `-o Port=NNNN` command line option.
 This can be overwritten as follows (in this order):
 
 1. Provide a `org.apache.sshd.server.subsystem.SubsystemFactory` system property containing comma-separated fully-qualified names of classes implementing
-this interface. The implementations must have a public no-args constructor for instantiating them. The order of the provided subsystems will be according
-to their order in the specified list
+this interface. The implementations must be public and have a public no-args constructor for instantiating them. The order of the provided subsystems will
+be according to their order in the specified list.
 
 2. Provide a `-o Subsystem=xxx,yyy` command line argument where value is a comma-separated list of the **name**(s) of the auto-detected factories via
 the `ServiceLoader` mechanism. The special value `none` may be used to indicate that no subsystem is to be configured. **Note:** no specific order is

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/04c57940/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
index 1d982f2..09b440d 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -37,6 +37,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
+import java.util.ServiceLoader;
 import java.util.TreeMap;
 import java.util.logging.Level;
 
@@ -61,6 +62,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.threads.ThreadUtils;
 
 /**
  * TODO Add javadoc
@@ -240,6 +242,51 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
         return sb;
     }
 
+    public static SftpClientFactory resolveSftpClientFactory(String... args) {
+        int numArgs = GenericUtils.length(args);
+        ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(SftpClientFactory.class);
+        for (int index = 0; index < numArgs; index++) {
+            String argVal = args[index];
+            if ("-o".equals(argVal)) {
+                index++;
+
+                String opt = args[index];
+                int idx = opt.indexOf('=');
+                ValidateUtils.checkTrue(idx > 0, "bad syntax for option: %s %s", argVal, opt);
+
+                String optName = opt.substring(0, idx);
+                String optValue = opt.substring(idx + 1);
+                if (!Objects.equals(SftpClientFactory.class.getSimpleName(), optName)) {
+                    continue;
+                }
+
+                try {
+                    Class<?> clazz = cl.loadClass(optValue);
+                    return SftpClientFactory.class.cast(clazz.newInstance());
+                } catch (Throwable t) {
+                    System.err.append("Failed (").append(t.getClass().getSimpleName()).append(')')
+                        .append(" to instantiate ").append(optValue)
+                        .append(": ").println(t.getMessage());
+                    System.err.flush();
+                    throw GenericUtils.toRuntimeException(t, true);
+                }
+            }
+        }
+
+        ServiceLoader<SftpClientFactory> loader = ServiceLoader.load(SftpClientFactory.class, cl);
+        SftpClientFactory factory = null;
+        for (SftpClientFactory f : loader) {
+            ValidateUtils.checkState(factory == null, "Multiple factories detected - select one");
+            factory = f;
+        }
+
+        if (factory != null) {
+            return factory;
+        }
+
+        return SftpClientFactory.instance();
+    }
+
     public static void main(String[] args) throws Exception {
         PrintStream stdout = System.out;
         PrintStream stderr = System.err;
@@ -262,7 +309,7 @@ public class SftpCommandMain extends SshClientCliSupport implements Channel {
 
             try {
                 // TODO allow command-line specification of SftpClientFactory
-                SftpClientFactory clientFactory = SftpClientFactory.instance();
+                SftpClientFactory clientFactory = resolveSftpClientFactory(args);
                 try (SftpClient sftpClient = clientFactory.createSftpClient(session);
                      SftpCommandMain sftp = new SftpCommandMain(sftpClient)) {
                     // TODO allow injection of extra CommandExecutor(s) via command line and/or service loading

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/04c57940/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
index 903128a..585f4a9 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
@@ -144,7 +144,7 @@ public abstract class SshServerCliSupport extends CliSupport {
             for (String fqcn : classes) {
                 try {
                     Class<?> clazz = cl.loadClass(fqcn);
-                    SubsystemFactory factory = (SubsystemFactory) clazz.newInstance();
+                    SubsystemFactory factory = SubsystemFactory.class.cast(clazz.newInstance());
                     subsystems.add(factory);
                 } catch (Throwable t) {
                     System.err.append("Failed (").append(t.getClass().getSimpleName()).append(')')


[6/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for client commands to sshd-cli module

Posted by lg...@apache.org.
[SSHD-816] Moved all 'main' code for client commands to sshd-cli module


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/536effdc
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/536effdc
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/536effdc

Branch: refs/heads/master
Commit: 536effdc5c1a9a0b8dcc0fc290b59fddd7efeb51
Parents: 76fe821
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Tue Apr 17 10:13:01 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Tue Apr 17 15:32:31 2018 +0300

----------------------------------------------------------------------
 README.md                                       |  14 +-
 assembly/pom.xml                                |   5 +
 assembly/src/main/distribution/bin/scp.bat      |   2 +-
 assembly/src/main/distribution/bin/scp.sh       |   2 +-
 assembly/src/main/distribution/bin/sftp.bat     |   2 +-
 assembly/src/main/distribution/bin/sftp.sh      |   2 +-
 .../src/main/distribution/bin/ssh-keyscan.bat   |   2 +-
 .../src/main/distribution/bin/ssh-keyscan.sh    |   2 +-
 assembly/src/main/distribution/bin/ssh.bat      |   2 +-
 assembly/src/main/distribution/bin/ssh.sh       |   2 +-
 pom.xml                                         |   1 +
 sshd-cli/pom.xml                                | 115 +++
 .../java/org/apache/sshd/cli/CliSupport.java    |  38 +
 .../apache/sshd/cli/client/ScpCommandMain.java  | 220 +++++
 .../sshd/cli/client/SftpCommandExecutor.java    |  34 +
 .../apache/sshd/cli/client/SftpCommandMain.java | 919 ++++++++++++++++++
 .../sshd/cli/client/SshClientCliSupport.java    | 632 +++++++++++++
 .../apache/sshd/cli/client/SshClientMain.java   | 193 ++++
 .../apache/sshd/cli/client/SshKeyScanMain.java  | 741 +++++++++++++++
 .../apache/sshd/cli/client/ChannelExecMain.java |  93 ++
 .../cli/client/ScpCommandMainDevelopment.java   |  36 +
 .../cli/client/SftpCommandMainDevelopment.java  |  36 +
 .../cli/client/SshClientMainDevelopment.java    |  36 +
 .../cli/client/SshKeyScanMainDevelopment.java   |  36 +
 .../java/org/apache/sshd/client/SshClient.java  | 740 ---------------
 .../java/org/apache/sshd/client/SshKeyScan.java | 740 ---------------
 .../sshd/client/scp/DefaultScpClient.java       | 184 ----
 .../org/apache/sshd/client/SshClientMain.java   |  36 -
 .../org/apache/sshd/client/SshKeyScanMain.java  |  36 -
 .../sshd/client/channel/ChannelExecMain.java    |  93 --
 .../apache/sshd/client/scp/ScpCommandMain.java  |  36 -
 .../sshd/client/subsystem/sftp/SftpCommand.java | 920 -------------------
 .../client/subsystem/sftp/SftpCommandMain.java  |  36 -
 33 files changed, 3155 insertions(+), 2831 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index ecf2317..bcef46f 100644
--- a/README.md
+++ b/README.md
@@ -1279,9 +1279,19 @@ in case the classes it used it are modified or deleted.
 
 ## Command line clients
 
-The _apache-sshd.zip_ distribution provides `Windows/Linux` scripts that use the MINA SSHD code base to implement the common _ssh, scp, sftp_ commands. The clients accept most useful switches from the original commands they mimic, where the `-o Option=Value` arguments can be used to configure the client/server in addition to the system properties mechanism. For more details, consult the _main_ methods code in the respective `SshClient`, `SftpCommand` and `DefaultScpClient` classes. The code also includes `SshKeyScan#main` that is a simple implementation for [ssh-keyscan(1)](https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1).
+The _apache-sshd.zip_ distribution provides `Windows/Linux` scripts that use the MINA SSHD code base to implement the common _ssh, scp, sftp_ commands. The clients accept most useful switches from the original commands they mimic, where the `-o Option=Value` arguments can be used to configure the client/server in addition to the system properties mechanism. For more details, consult the _main_ methods code in the respective `SshClientMain`, `SftpCommandMain` and `ScpClientMain` classes. The code also includes `SshKeyScanMain` that is a simple implementation for [ssh-keyscan(1)](https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1).
 
-The distribution also includes also an _sshd_ script that can be used to launch a server instance - see `SshServer#main` for activation command line arguments and options.
+The distribution also includes also an _sshd_ script that can be used to launch a server instance - see `SshServerMain#main` for activation command line arguments and options.
+
+In order to use this CLI code as part of another project, one needs to include the _sshd-cli_ module:
+
+```xml
+    <dependency>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd-cli</artifactId>
+        <version>...same version as the core...</version>
+    </dependency>
+```
 
 ## GIT support
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/pom.xml
----------------------------------------------------------------------
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 85eab19..f3c4716 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -47,6 +47,11 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-jdk14</artifactId>
         </dependency>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/scp.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/scp.bat b/assembly/src/main/distribution/bin/scp.bat
index ce86067..68eea30 100644
--- a/assembly/src/main/distribution/bin/scp.bat
+++ b/assembly/src/main/distribution/bin/scp.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.scp.DefaultScpClient %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.ScpCommandMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/scp.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/scp.sh b/assembly/src/main/distribution/bin/scp.sh
index 55d26b8..0d28c89 100644
--- a/assembly/src/main/distribution/bin/scp.sh
+++ b/assembly/src/main/distribution/bin/scp.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.scp.DefaultScpClient "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.ScpCommandMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/sftp.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sftp.bat b/assembly/src/main/distribution/bin/sftp.bat
index f4c5aee..3a1b908 100644
--- a/assembly/src/main/distribution/bin/sftp.bat
+++ b/assembly/src/main/distribution/bin/sftp.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.subsystem.sftp.SftpCommand %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SftpCommandMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/sftp.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sftp.sh b/assembly/src/main/distribution/bin/sftp.sh
index 8884ce8..477e7fe 100644
--- a/assembly/src/main/distribution/bin/sftp.sh
+++ b/assembly/src/main/distribution/bin/sftp.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.subsystem.sftp.SftpCommand "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SftpCommandMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh-keyscan.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh-keyscan.bat b/assembly/src/main/distribution/bin/ssh-keyscan.bat
index bbbd1e0..1c2b78e 100644
--- a/assembly/src/main/distribution/bin/ssh-keyscan.bat
+++ b/assembly/src/main/distribution/bin/ssh-keyscan.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.SshKeyScan %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SshKeyScanMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh-keyscan.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh-keyscan.sh b/assembly/src/main/distribution/bin/ssh-keyscan.sh
index e6245e0..083f7b8 100644
--- a/assembly/src/main/distribution/bin/ssh-keyscan.sh
+++ b/assembly/src/main/distribution/bin/ssh-keyscan.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.SshKeyScan "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SshKeyScanMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh.bat b/assembly/src/main/distribution/bin/ssh.bat
index 14b5b41..85e93c2 100644
--- a/assembly/src/main/distribution/bin/ssh.bat
+++ b/assembly/src/main/distribution/bin/ssh.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.SshClient %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SshClientMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh.sh b/assembly/src/main/distribution/bin/ssh.sh
index 3e29813..fe07901 100644
--- a/assembly/src/main/distribution/bin/ssh.sh
+++ b/assembly/src/main/distribution/bin/ssh.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.SshClient "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SshClientMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index ea73bb4..9cbf957 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1010,6 +1010,7 @@
         <module>sshd-git</module>
         <module>sshd-contrib</module>
         <module>sshd-spring-sftp</module>
+        <module>sshd-cli</module>
         <module>assembly</module>
     </modules>
 </project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-cli/pom.xml b/sshd-cli/pom.xml
new file mode 100644
index 0000000..bd19a7e
--- /dev/null
+++ b/sshd-cli/pom.xml
@@ -0,0 +1,115 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>1.7.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-cli</artifactId>
+    <name>Apache Mina SSHD :: CLI</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2018</inceptionYear>
+
+    <properties>
+        <projectRoot>${project.basedir}/..</projectRoot>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-sftp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jzlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/filtered-resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
new file mode 100644
index 0000000..14737bb
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package org.apache.sshd.cli;
+
+import java.io.PrintStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class CliSupport {
+    protected CliSupport() {
+        super();
+    }
+
+    public static boolean showError(PrintStream stderr, String message) {
+        stderr.println(message);
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
new file mode 100644
index 0000000..770ab32
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.scp.ScpClient.Option;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpLocation;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpCommandMain extends SshClientCliSupport {
+    /**
+     * Command line option used to indicate a non-default port
+     */
+    public static final String SCP_PORT_OPTION = "-P";
+
+    public ScpCommandMain() {
+        super();    // in case someone wants to extend it
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static String[] normalizeCommandArguments(PrintStream stdout, PrintStream stderr, String... args) {
+        int numArgs = GenericUtils.length(args);
+        if (numArgs <= 0) {
+            return args;
+        }
+
+        List<String> effective = new ArrayList<>(numArgs);
+        boolean error = false;
+        for (int index = 0; (index < numArgs) && (!error); index++) {
+            String argName = args[index];
+            // handled by 'setupClientSession'
+            if (isArgumentedOption(SCP_PORT_OPTION, argName)) {
+                if ((index + 1) >= numArgs) {
+                    error = showError(stderr, "option requires an argument: " + argName);
+                    break;
+                }
+
+                effective.add(argName);
+                effective.add(args[++index]);
+            } else if ("-r".equals(argName) || "-p".equals(argName)
+                    || "-q".equals(argName) || "-C".equals(argName)
+                    || "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) {
+                effective.add(argName);
+            } else if (argName.charAt(0) == '-') {
+                error = showError(stderr, "Unknown option: " + argName);
+                break;
+            } else {
+                if ((index + 1) >= numArgs) {
+                    error = showError(stderr, "Not enough arguments");
+                    break;
+                }
+
+                ScpLocation source = new ScpLocation(argName);
+                ScpLocation target = new ScpLocation(args[++index]);
+                if (index < (numArgs - 1)) {
+                    error = showError(stderr, "Unexpected extra arguments");
+                    break;
+                }
+
+                if (source.isLocal() == target.isLocal()) {
+                    error = showError(stderr, "Both targets are either remote or local");
+                    break;
+                }
+
+                ScpLocation remote = source.isLocal() ? target : source;
+                effective.add(remote.resolveUsername() + "@" + remote.getHost());
+                effective.add(source.toString());
+                effective.add(target.toString());
+                break;
+            }
+        }
+
+        if (error) {
+            return null;
+        }
+
+        return effective.toArray(new String[effective.size()]);
+    }
+
+    public static void main(String[] args) throws Exception {
+        final PrintStream stdout = System.out;
+        final PrintStream stderr = System.err;
+        OutputStream logStream = stdout;
+        try (BufferedReader stdin = new BufferedReader(
+                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
+            args = normalizeCommandArguments(stdout, stderr, args);
+            int numArgs = GenericUtils.length(args);
+            // see the way normalizeCommandArguments works...
+            if (numArgs >= 2) {
+                Level level = resolveLoggingVerbosity(args, numArgs - 2);
+                logStream = resolveLoggingTargetStream(stdout, stderr, args, numArgs - 2);
+                if (logStream != null) {
+                    setupLogging(level, stdout, stderr, logStream);
+                }
+            }
+
+            ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
+                ? null : setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
+            if (session == null) {
+                stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity]"
+                         + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
+                         + " [-c cipherlist] [-m maclist] [-w password] [-C] <source> <target>");
+                stderr.println();
+                stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path");
+                stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local");
+                System.exit(-1);
+                return; // not that we really need it...
+            }
+
+            try {
+                // see the way normalizeCommandArguments works...
+                Collection<Option> options = EnumSet.noneOf(Option.class);
+                boolean quiet = false;
+                for (int index = 0; index < numArgs; index++) {
+                    String argName = args[index];
+                    if ("-r".equals(argName)) {
+                        options.add(Option.Recursive);
+                    } else if ("-p".equals(argName)) {
+                        options.add(Option.PreserveAttributes);
+                    } else if ("-q".equals(argName)) {
+                        quiet = true;
+                    }
+                }
+
+                if (!quiet) {
+                    session.setScpTransferEventListener(new ScpTransferEventListener() {
+                        @Override
+                        public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
+                            logEvent("startFolderEvent", op, file, -1L, perms, null);
+                        }
+
+                        @Override
+                        public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
+                            logEvent("endFolderEvent", op, file, -1L, perms, thrown);
+                        }
+
+                        @Override
+                        public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
+                            logEvent("startFileEvent", op, file, length, perms, null);
+                        }
+
+                        @Override
+                        public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
+                            logEvent("endFileEvent", op, file, length, perms, thrown);
+                        }
+
+                        private void logEvent(String name, FileOperation op, Path file, long length, Collection<PosixFilePermission> perms, Throwable thrown) {
+                            PrintStream ps = (thrown == null) ? stdout : stderr;
+                            ps.append('\t').append(name).append('[').append(op.name()).append(']').append(' ').append(file.toString());
+                            if (length > 0L) {
+                                ps.append(' ').append("length=").append(Long.toString(length));
+                            }
+                            ps.append(' ').append(String.valueOf(perms));
+
+                            if (thrown != null) {
+                                ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage());
+                            }
+                            ps.println();
+                        }
+                    });
+                }
+
+                ScpClient client = session.createScpClient();
+                ScpLocation source = new ScpLocation(args[numArgs - 2]);
+                ScpLocation target = new ScpLocation(args[numArgs - 1]);
+                if (source.isLocal()) {
+                    client.upload(source.getPath(), target.getPath(), options);
+                } else {
+                    client.download(source.getPath(), target.getPath(), options);
+                }
+            } finally {
+                session.close();
+            }
+        } finally {
+            if ((logStream != stdout) && (logStream != stderr)) {
+                logStream.close();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java
new file mode 100644
index 0000000..fb1a762
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.PrintStream;
+
+import org.apache.sshd.common.NamedResource;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpCommandExecutor extends NamedResource {
+    // return value is whether to stop running
+    boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
new file mode 100644
index 0000000..1d982f2
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -0,0 +1,919 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.channels.Channel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpCommandMain extends SshClientCliSupport implements Channel {
+    /**
+     * Command line option used to indicate a non-default port number
+     */
+    public static final String SFTP_PORT_OPTION = "-P";
+
+    private final SftpClient client;
+    private final Map<String, SftpCommandExecutor> commandsMap;
+    private String cwdRemote;
+    private String cwdLocal;
+
+    public SftpCommandMain(SftpClient client) {
+        this.client = Objects.requireNonNull(client, "No client");
+
+        Map<String, SftpCommandExecutor> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (SftpCommandExecutor e : Arrays.asList(
+                new ExitCommandExecutor(),
+                new PwdCommandExecutor(),
+                new InfoCommandExecutor(),
+                new SessionCommandExecutor(),
+                new VersionCommandExecutor(),
+                new CdCommandExecutor(),
+                new LcdCommandExecutor(),
+                new MkdirCommandExecutor(),
+                new LsCommandExecutor(),
+                new LStatCommandExecutor(),
+                new ReadLinkCommandExecutor(),
+                new RmCommandExecutor(),
+                new RmdirCommandExecutor(),
+                new RenameCommandExecutor(),
+                new StatVfsCommandExecutor(),
+                new GetCommandExecutor(),
+                new PutCommandExecutor(),
+                new HelpCommandExecutor()
+        )) {
+            String name = e.getName();
+            ValidateUtils.checkTrue(map.put(name, e) == null, "Multiple commands named '%s'", name);
+        }
+        commandsMap = Collections.unmodifiableMap(map);
+        cwdLocal = System.getProperty("user.dir");
+    }
+
+    public final SftpClient getClient() {
+        return client;
+    }
+
+    public void doInteractive(BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        SftpClient sftp = getClient();
+        setCurrentRemoteDirectory(sftp.canonicalPath("."));
+        while (true) {
+            stdout.append(getCurrentRemoteDirectory()).append(" > ").flush();
+            String line = stdin.readLine();
+            if (line == null) { // EOF
+                break;
+            }
+
+            line = GenericUtils.replaceWhitespaceAndTrim(line);
+            if (GenericUtils.isEmpty(line)) {
+                continue;
+            }
+
+            String cmd;
+            String args;
+            int pos = line.indexOf(' ');
+            if (pos > 0) {
+                cmd = line.substring(0, pos);
+                args = line.substring(pos + 1).trim();
+            } else {
+                cmd = line;
+                args = "";
+            }
+
+            SftpCommandExecutor exec = commandsMap.get(cmd);
+            try {
+                if (exec == null) {
+                    stderr.append("Unknown command: ").println(line);
+                } else {
+                    try {
+                        if (exec.executeCommand(args, stdin, stdout, stderr)) {
+                            break;
+                        }
+                    } catch (Exception e) {
+                        stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
+                    } finally {
+                        stdout.flush();
+                    }
+                }
+            } finally {
+                stderr.flush(); // just makings sure
+            }
+        }
+    }
+
+    protected String resolveLocalPath(String pathArg) {
+        String cwd = getCurrentLocalDirectory();
+        if (GenericUtils.isEmpty(pathArg)) {
+            return cwd;
+        }
+
+        if (OsUtils.isWin32()) {
+            if ((pathArg.length() >= 2) && (pathArg.charAt(1) == ':')) {
+                return pathArg;
+            }
+        } else {
+            if (pathArg.charAt(0) == '/') {
+                return pathArg;
+            }
+        }
+
+        return cwd + File.separator + pathArg.replace('/', File.separatorChar);
+    }
+
+    protected String resolveRemotePath(String pathArg) {
+        String cwd = getCurrentRemoteDirectory();
+        if (GenericUtils.isEmpty(pathArg)) {
+            return cwd;
+        }
+
+        if (pathArg.charAt(0) == '/') {
+            return pathArg;
+        } else {
+            return cwd + "/" + pathArg;
+        }
+    }
+
+    protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException {
+        stdout.append('\t').append(Long.toString(attrs.getSize()))
+              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
+        if (attrs.isSymbolicLink()) {
+            String linkValue = sftp.readLink(path);
+            stdout.append(" => ")
+                  .append('(').append(attrs.isDirectory() ? "dir" : "file").append(')')
+                  .append(' ').append(linkValue);
+        }
+
+        return stdout;
+    }
+
+    public String getCurrentRemoteDirectory() {
+        return cwdRemote;
+    }
+
+    public void setCurrentRemoteDirectory(String path) {
+        cwdRemote = path;
+    }
+
+    public String getCurrentLocalDirectory() {
+        return cwdLocal;
+    }
+
+    public void setCurrentLocalDirectory(String path) {
+        cwdLocal = path;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return client.isOpen();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (isOpen()) {
+            client.close();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static <A extends Appendable> A appendInfoValue(A sb, CharSequence name, Object value) throws IOException {
+        sb.append('\t').append(name).append(": ").append(Objects.toString(value));
+        return sb;
+    }
+
+    public static void main(String[] args) throws Exception {
+        PrintStream stdout = System.out;
+        PrintStream stderr = System.err;
+        OutputStream logStream = stderr;
+        try (BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
+            Level level = resolveLoggingVerbosity(args);
+            logStream = resolveLoggingTargetStream(stdout, stderr, args);
+            if (logStream != null) {
+                setupLogging(level, stdout, stderr, logStream);
+            }
+
+            ClientSession session = (logStream == null) ? null : setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args);
+            if (session == null) {
+                System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i identity]"
+                        + " [-l login] [" + SFTP_PORT_OPTION + " port] [-o option=value]"
+                        + " [-w password] [-c cipherlist]  [-m maclist] [-C] hostname/user@host");
+                System.exit(-1);
+                return;
+            }
+
+            try {
+                // TODO allow command-line specification of SftpClientFactory
+                SftpClientFactory clientFactory = SftpClientFactory.instance();
+                try (SftpClient sftpClient = clientFactory.createSftpClient(session);
+                     SftpCommandMain sftp = new SftpCommandMain(sftpClient)) {
+                    // TODO allow injection of extra CommandExecutor(s) via command line and/or service loading
+                    sftp.doInteractive(stdin, stdout, stderr);
+                }
+            } finally {
+                session.close();
+            }
+        } finally {
+            if ((logStream != stdout) && (logStream != stderr)) {
+                logStream.close();
+            }
+        }
+    }
+
+    private static class ExitCommandExecutor implements SftpCommandExecutor {
+        ExitCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "exit";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            stdout.println("Exiting");
+            return true;
+        }
+    }
+
+    private class PwdCommandExecutor implements SftpCommandExecutor {
+        protected PwdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "pwd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            stdout.append('\t').append("Remote: ").println(getCurrentRemoteDirectory());
+            stdout.append('\t').append("Local: ").println(getCurrentLocalDirectory());
+            return false;
+        }
+    }
+
+    private class SessionCommandExecutor implements SftpCommandExecutor {
+        SessionCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "session";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            ClientSession session = sftp.getSession();
+            appendInfoValue(stdout, "Session ID", BufferUtils.toHex(session.getSessionId())).println();
+            appendInfoValue(stdout, "Connect address", session.getConnectAddress()).println();
+
+            IoSession ioSession = session.getIoSession();
+            appendInfoValue(stdout, "Local address", ioSession.getLocalAddress()).println();
+            appendInfoValue(stdout, "Remote address", ioSession.getRemoteAddress()).println();
+
+            for (KexProposalOption option : KexProposalOption.VALUES) {
+                appendInfoValue(stdout, option.getDescription(), session.getNegotiatedKexParameter(option)).println();
+            }
+
+            return false;
+        }
+    }
+
+    private class InfoCommandExecutor implements SftpCommandExecutor {
+        InfoCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "info";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            Session session = sftp.getSession();
+            stdout.append('\t').println(session.getServerVersion());
+
+            Map<String, byte[]> extensions = sftp.getServerExtensions();
+            Map<String, ?> parsed = ParserUtils.parse(extensions);
+            if (GenericUtils.size(extensions) > 0) {
+                stdout.println();
+            }
+
+            extensions.forEach((name, value) -> {
+                Object info = parsed.get(name);
+
+                stdout.append('\t').append(name).append(": ");
+                if (info == null) {
+                    stdout.println(BufferUtils.toHex(value));
+                } else {
+                    stdout.println(info);
+                }
+            });
+
+            return false;
+        }
+    }
+
+    private class VersionCommandExecutor implements SftpCommandExecutor {
+        VersionCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "version";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            stdout.append('\t').println(sftp.getVersion());
+            return false;
+        }
+    }
+
+    private class CdCommandExecutor extends PwdCommandExecutor {
+        CdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "cd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String newPath = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            setCurrentRemoteDirectory(sftp.canonicalPath(newPath));
+            return super.executeCommand("", stdin, stdout, stderr);
+        }
+    }
+
+    private class LcdCommandExecutor extends PwdCommandExecutor {
+        LcdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "lcd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            if (GenericUtils.isEmpty(args)) {
+                setCurrentLocalDirectory(System.getProperty("user.home"));
+            } else {
+                Path path = Paths.get(resolveLocalPath(args)).normalize().toAbsolutePath();
+                ValidateUtils.checkTrue(Files.exists(path), "No such local directory: %s", path);
+                ValidateUtils.checkTrue(Files.isDirectory(path), "Path is not a directory: %s", path);
+                setCurrentLocalDirectory(path.toString());
+            }
+
+            return super.executeCommand("", stdin, stdout, stderr);
+        }
+    }
+
+    private class MkdirCommandExecutor implements SftpCommandExecutor {
+        MkdirCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "mkdir";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String path = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            sftp.mkdir(path);
+            return false;
+        }
+    }
+
+    private class LsCommandExecutor implements SftpCommandExecutor {
+        LsCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "ls";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numComps = GenericUtils.length(comps);
+            String pathArg = (numComps <= 0) ? null : GenericUtils.trimToEmpty(comps[numComps - 1]);
+            String flags = (numComps >= 2) ? GenericUtils.trimToEmpty(comps[0]) : null;
+            // ignore all flags
+            if ((GenericUtils.length(pathArg) > 0) && (pathArg.charAt(0) == '-')) {
+                flags = pathArg;
+                pathArg = null;
+            }
+
+            String path = resolveRemotePath(pathArg);
+            SftpClient sftp = getClient();
+            int version = sftp.getVersion();
+            boolean showLongName = (version == SftpConstants.SFTP_V3) && (GenericUtils.length(flags) > 1) && (flags.indexOf('l') > 0);
+            for (SftpClient.DirEntry entry : sftp.readDir(path)) {
+                String fileName = entry.getFilename();
+                SftpClient.Attributes attrs = entry.getAttributes();
+                appendFileAttributes(stdout.append('\t').append(fileName), sftp, path + "/" + fileName, attrs).println();
+                if (showLongName) {
+                    stdout.append("\t\tlong-name: ").println(entry.getLongFilename());
+                }
+            }
+
+            return false;
+        }
+    }
+
+    private class RmCommandExecutor implements SftpCommandExecutor {
+        RmCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rm";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs >= 1, "No arguments");
+            ValidateUtils.checkTrue(numArgs <= 2, "Too many arguments: %s", args);
+
+            String remotePath = comps[0];
+            boolean recursive = false;
+            boolean verbose = false;
+            if (remotePath.charAt(0) == '-') {
+                ValidateUtils.checkTrue(remotePath.length() > 1, "Missing flags specification: %s", args);
+                ValidateUtils.checkTrue(numArgs == 2, "Missing remote directory: %s", args);
+
+                for (int index = 1; index < remotePath.length(); index++) {
+                    char ch = remotePath.charAt(index);
+                    switch(ch) {
+                        case 'r' :
+                            recursive = true;
+                            break;
+                        case 'v':
+                            verbose = true;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
+                    }
+                }
+                remotePath = comps[1];
+            }
+
+            String path = resolveRemotePath(remotePath);
+            SftpClient sftp = getClient();
+            if (recursive) {
+                Attributes attrs = sftp.stat(path);
+                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", args);
+                removeRecursive(sftp, path, attrs, stdout, verbose);
+            } else {
+                sftp.remove(path);
+                if (verbose) {
+                    stdout.append('\t').append("Removed ").println(path);
+                }
+            }
+
+            return false;
+        }
+
+        private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+            if (attrs.isDirectory()) {
+                for (DirEntry entry : sftp.readDir(path)) {
+                    String name = entry.getFilename();
+                    if (".".equals(name) || "..".equals(name)) {
+                        continue;
+                    }
+
+                    removeRecursive(sftp, path + "/" + name, entry.getAttributes(), stdout, verbose);
+                }
+
+                sftp.rmdir(path);
+            } else if (attrs.isRegularFile()) {
+                sftp.remove(path);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip special file ").println(path);
+                    return;
+                }
+            }
+
+            if (verbose) {
+                stdout.append('\t').append("Removed ").println(path);
+            }
+        }
+    }
+
+    private class RmdirCommandExecutor implements SftpCommandExecutor {
+        RmdirCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rmdir";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String path = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            sftp.rmdir(path);
+            return false;
+        }
+    }
+
+    private class RenameCommandExecutor implements SftpCommandExecutor {
+        RenameCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rename";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args);
+
+            String oldPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[0]));
+            String newPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[1]));
+            SftpClient sftp = getClient();
+            sftp.rename(oldPath, newPath);
+            return false;
+        }
+    }
+
+    private class StatVfsCommandExecutor implements SftpCommandExecutor {
+        StatVfsCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return StatVfsExtensionParser.NAME;
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args);
+
+            SftpClient sftp = getClient();
+            OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
+            ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
+
+            String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
+            OpenSSHStatExtensionInfo info = ext.stat(remPath);
+            Field[] fields = info.getClass().getFields();
+            for (Field f : fields) {
+                String name = f.getName();
+                int mod = f.getModifiers();
+                if (Modifier.isStatic(mod)) {
+                    continue;
+                }
+
+                Object value = f.get(info);
+                stdout.append('\t').append(name).append(": ").println(value);
+            }
+
+            return false;
+        }
+    }
+
+    private class LStatCommandExecutor implements SftpCommandExecutor {
+        LStatCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "lstat";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
+
+            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
+            SftpClient client = getClient();
+            Attributes attrs = client.lstat(path);
+            appendFileAttributes(stdout, client, path, attrs).println();
+            return false;
+        }
+    }
+
+    private class ReadLinkCommandExecutor implements SftpCommandExecutor {
+        ReadLinkCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "readlink";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
+
+            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
+            SftpClient client = getClient();
+            String linkData = client.readLink(path);
+            stdout.append('\t').println(linkData);
+            return false;
+        }
+    }
+
+    private class HelpCommandExecutor implements SftpCommandExecutor {
+        HelpCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "help";
+        }
+
+        @Override
+        @SuppressWarnings("synthetic-access")
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            for (String cmd : commandsMap.keySet()) {
+                stdout.append('\t').println(cmd);
+            }
+            return false;
+        }
+    }
+
+    private abstract class TransferCommandExecutor implements SftpCommandExecutor {
+        protected TransferCommandExecutor() {
+            super();
+        }
+
+        protected void createDirectories(SftpClient sftp, String remotePath) throws IOException {
+            try {
+                Attributes attrs = sftp.stat(remotePath);
+                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path already exists but is not a directory: %s", remotePath);
+                return;
+            } catch (SftpException e) {
+                int status = e.getStatus();
+                ValidateUtils.checkTrue(status == SftpConstants.SSH_FX_NO_SUCH_FILE, "Failed to get status of %s: %s", remotePath, e.getMessage());
+            }
+
+            int pos = remotePath.lastIndexOf('/');
+            ValidateUtils.checkTrue(pos > 0, "No more parents for %s", remotePath);
+            createDirectories(sftp, remotePath.substring(0, pos));
+        }
+
+        protected void transferFile(SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose) throws IOException {
+            // Create the file's hierarchy
+            if (upload) {
+                int pos = remotePath.lastIndexOf('/');
+                ValidateUtils.checkTrue(pos > 0, "Missing full remote file path: %s", remotePath);
+                createDirectories(sftp, remotePath.substring(0, pos));
+            } else {
+                Files.createDirectories(localPath.getParent());
+            }
+
+            try (InputStream input = upload ? Files.newInputStream(localPath) : sftp.read(remotePath);
+                 OutputStream output = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath)) {
+                IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE);
+            }
+
+            if (verbose) {
+                stdout.append('\t')
+                      .append("Copied ").append(upload ? localPath.toString() : remotePath)
+                      .append(" to ").println(upload ? remotePath : localPath.toString());
+            }
+        }
+
+        protected void transferRemoteDir(SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+            if (attrs.isDirectory()) {
+                for (DirEntry entry : sftp.readDir(remotePath)) {
+                    String name = entry.getFilename();
+                    if (".".equals(name) || "..".equals(name)) {
+                        continue;
+                    }
+
+                    transferRemoteDir(sftp, localPath.resolve(name), remotePath + "/" + name, entry.getAttributes(), stdout, verbose);
+                }
+            } else if (attrs.isRegularFile()) {
+                transferFile(sftp, localPath, remotePath, false, stdout, verbose);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip remote special file ").println(remotePath);
+                }
+            }
+        }
+
+        protected void transferLocalDir(SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose) throws IOException {
+            if (Files.isDirectory(localPath)) {
+                try (DirectoryStream<Path> ds = Files.newDirectoryStream(localPath)) {
+                    for (Path entry : ds) {
+                        String name = entry.getFileName().toString();
+                        transferLocalDir(sftp, localPath.resolve(name), remotePath + "/" + name, stdout, verbose);
+                    }
+                }
+            } else if (Files.isRegularFile(localPath)) {
+                transferFile(sftp, localPath, remotePath, true, stdout, verbose);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip local special file ").println(localPath);
+                }
+            }
+        }
+
+        protected void executeCommand(String args, boolean upload, PrintStream stdout) throws IOException {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue((numArgs >= 1) && (numArgs <= 3), "Invalid number of arguments: %s", args);
+
+            String src = comps[0];
+            boolean recursive = false;
+            boolean verbose = false;
+            int tgtIndex = 1;
+            if (src.charAt(0) == '-') {
+                ValidateUtils.checkTrue(src.length() > 1, "Missing flags specification: %s", args);
+                ValidateUtils.checkTrue(numArgs >= 2, "Missing source specification: %s", args);
+
+                for (int index = 1; index < src.length(); index++) {
+                    char ch = src.charAt(index);
+                    switch(ch) {
+                        case 'r' :
+                            recursive = true;
+                            break;
+                        case 'v':
+                            verbose = true;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
+                    }
+                }
+                src = comps[1];
+                tgtIndex++;
+            }
+
+            String tgt = (tgtIndex < numArgs) ? comps[tgtIndex] : null;
+            String localPath;
+            String remotePath;
+            if (upload) {
+                localPath = src;
+                remotePath = ValidateUtils.checkNotNullAndNotEmpty(tgt, "No remote target specified: %s", args);
+            } else {
+                localPath = GenericUtils.isEmpty(tgt) ? getCurrentLocalDirectory() : tgt;
+                remotePath = src;
+            }
+
+            SftpClient sftp = getClient();
+            Path local = Paths.get(resolveLocalPath(localPath)).normalize().toAbsolutePath();
+            String remote = resolveRemotePath(remotePath);
+            if (recursive) {
+                if (upload) {
+                    ValidateUtils.checkTrue(Files.isDirectory(local), "Local path not a directory or does not exist: %s", local);
+                    transferLocalDir(sftp, local, remote, stdout, verbose);
+                } else {
+                    Attributes attrs = sftp.stat(remote);
+                    ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", remote);
+                    transferRemoteDir(sftp, local, remote, attrs, stdout, verbose);
+                }
+            } else {
+                if (Files.exists(local) && Files.isDirectory(local)) {
+                    int pos = remote.lastIndexOf('/');
+                    String name = (pos >= 0) ? remote.substring(pos + 1) : remote;
+                    local = local.resolve(name);
+                }
+
+                transferFile(sftp, local, remote, upload, stdout, verbose);
+            }
+        }
+    }
+
+    private class GetCommandExecutor extends TransferCommandExecutor {
+        GetCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "get";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            executeCommand(args, false, stdout);
+            return false;
+        }
+    }
+
+    private class PutCommandExecutor extends TransferCommandExecutor {
+        PutCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "put";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            executeCommand(args, true, stdout);
+            return false;
+        }
+    }
+}


[4/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for client commands to sshd-cli module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 0874b93..1d034c1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -18,44 +18,24 @@
  */
 package org.apache.sshd.client;
 
-import java.io.BufferedReader;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.PrintWriter;
 import java.io.StreamCorruptedException;
-import java.io.StringWriter;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
 import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
-import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.TreeMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Formatter;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.logging.Logger;
 
 import org.apache.sshd.agent.SshAgentFactory;
 import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
@@ -65,18 +45,12 @@ import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
-import org.apache.sshd.client.channel.ChannelShell;
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.channel.ClientChannelEvent;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
-import org.apache.sshd.client.config.keys.ClientIdentity;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
 import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.future.DefaultConnectFuture;
-import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
-import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.AbstractClientSession;
 import org.apache.sshd.client.session.ClientConnectionServiceFactory;
@@ -89,40 +63,22 @@ import org.apache.sshd.client.simple.AbstractSimpleClientSessionCreator;
 import org.apache.sshd.client.simple.SimpleClient;
 import org.apache.sshd.common.Closeable;
 import org.apache.sshd.common.Factory;
-import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.PropertyResolver;
-import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.ServiceFactory;
 import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.cipher.BuiltinCiphers;
-import org.apache.sshd.common.cipher.Cipher;
-import org.apache.sshd.common.compression.BuiltinCompressions;
-import org.apache.sshd.common.compression.Compression;
-import org.apache.sshd.common.config.CompressionConfigValue;
-import org.apache.sshd.common.config.SshConfigFileReader;
-import org.apache.sshd.common.config.keys.BuiltinIdentities;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.helpers.AbstractFactoryManager;
 import org.apache.sshd.common.io.IoConnectFuture;
 import org.apache.sshd.common.io.IoConnector;
 import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.mac.BuiltinMacs;
-import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.scp.ScpFileOpener;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.common.util.io.NoCloseOutputStream;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
@@ -180,11 +136,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     public static final Factory<SshClient> DEFAULT_SSH_CLIENT_FACTORY = SshClient::new;
 
     /**
-     * Command line option used to indicate non-default target port
-     */
-    public static final String SSH_CLIENT_PORT_OPTION = "-p";
-
-    /**
      * Default user authentication preferences if not set
      * @see <A HREF="http://linux.die.net/man/5/ssh_config">ssh_config(5) - PreferredAuthentications</A>
      */
@@ -821,695 +772,4 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     public static SshClient setUpDefaultClient() {
         return ClientBuilder.builder().build();
     }
-
-    /*=================================
-          Main class implementation
-     *=================================*/
-
-    public static boolean showError(PrintStream stderr, String message) {
-        stderr.println(message);
-        return true;
-    }
-
-    public static boolean isArgumentedOption(String portOption, String argName) {
-        return portOption.equals(argName)
-             || "-i".equals(argName)
-             || "-o".equals(argName)
-             || "-l".equals(argName)
-             || "-w".equals(argName)
-             || "-c".equals(argName)
-             || "-m".equals(argName)
-             || "-E".equals(argName);
-    }
-
-    // NOTE: ClientSession#getFactoryManager is the SshClient
-    public static ClientSession setupClientSession(
-            String portOption, BufferedReader stdin, PrintStream stdout, PrintStream stderr, String... args)
-                    throws Exception {
-
-        int port = -1;
-        String host = null;
-        String login = null;
-        String password = null;
-        boolean error = false;
-        List<Path> identities = new ArrayList<>();
-        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        List<NamedFactory<Cipher>> ciphers = null;
-        List<NamedFactory<Mac>> macs = null;
-        List<NamedFactory<Compression>> compressions = null;
-        int numArgs = GenericUtils.length(args);
-        for (int i = 0; (!error) && (i < numArgs); i++) {
-            String argName = args[i];
-            String argVal = null;
-            if (isArgumentedOption(portOption, argName)) {
-                if ((i + 1) >= numArgs) {
-                    error = showError(stderr, "option requires an argument: " + argName);
-                    break;
-                }
-
-                argVal = args[++i];
-            }
-
-            if (portOption.equals(argName)) {
-                if (port > 0) {
-                    error = showError(stderr, argName + " option value re-specified: " + port);
-                    break;
-                }
-
-                port = Integer.parseInt(argVal);
-                if (port <= 0) {
-                    error = showError(stderr, "Bad option value for " + argName + ": " + port);
-                    break;
-                }
-            } else if ("-w".equals(argName)) {
-                if (GenericUtils.length(password) > 0) {
-                    error = showError(stderr, argName + " option value re-specified: " + password);
-                    break;
-                }
-                password = argVal;
-            } else if ("-c".equals(argName)) {
-                ciphers = setupCiphers(argName, argVal, ciphers, stderr);
-                if (GenericUtils.isEmpty(ciphers)) {
-                    error = true;
-                    break;
-                }
-            } else if ("-m".equals(argName)) {
-                macs = setupMacs(argName, argVal, macs, stderr);
-                if (GenericUtils.isEmpty(macs)) {
-                    error = true;
-                    break;
-                }
-            } else if ("-i".equals(argName)) {
-                identities.add(resolveIdentityFile(argVal));
-            } else if ("-C".equals(argName)) {
-                compressions = setupCompressions(argName,
-                        GenericUtils.join(
-                                Arrays.asList(
-                                        BuiltinCompressions.Constants.ZLIB, BuiltinCompressions.Constants.DELAYED_ZLIB), ','),
-                        compressions, stderr);
-                if (GenericUtils.isEmpty(compressions)) {
-                    error = true;
-                    break;
-                }
-            } else if ("-o".equals(argName)) {
-                String opt = argVal;
-                int idx = opt.indexOf('=');
-                if (idx <= 0) {
-                    error = showError(stderr, "bad syntax for option: " + opt);
-                    break;
-                }
-
-                String optName = opt.substring(0, idx);
-                String optValue = opt.substring(idx + 1);
-                if (HostConfigEntry.IDENTITY_FILE_CONFIG_PROP.equals(optName)) {
-                    identities.add(resolveIdentityFile(optValue));
-                } else {
-                    options.put(optName, optValue);
-                }
-            } else if ("-l".equals(argName)) {
-                if (login != null) {
-                    error = showError(stderr, argName + " option value re-specified: " + port);
-                    break;
-                }
-
-                login = argVal;
-            } else if (argName.charAt(0) != '-') {
-                if (host != null) { // assume part of a command following it
-                    break;
-                }
-
-                host = argName;
-                int pos = host.indexOf('@');  // check if user@host
-                if (pos > 0) {
-                    if (login == null) {
-                        login = host.substring(0, pos);
-                        host = host.substring(pos + 1);
-                    } else {
-                        error = showError(stderr, "Login already specified using -l option (" + login + "): " + host);
-                        break;
-                    }
-                }
-            }
-        }
-
-        if ((!error) && GenericUtils.isEmpty(host)) {
-            error = showError(stderr, "Hostname not specified");
-        }
-
-        if (error) {
-            return null;
-        }
-
-        SshClient client = setupClient(options, ciphers, macs, compressions, identities, stdin, stdout, stderr);
-        if (client == null) {
-            return null;
-        }
-
-        try {
-            client.start();
-
-            if (login == null) {
-                login = OsUtils.getCurrentUser();
-            }
-
-            if (port <= 0) {
-                port = SshConfigFileReader.DEFAULT_PORT;
-            }
-
-            // TODO use a configurable wait time
-            ClientSession session = client.connect(login, host, port).verify().getSession();
-            try {
-                if (GenericUtils.length(password) > 0) {
-                    session.addPasswordIdentity(password);
-                }
-                session.auth().verify(FactoryManager.DEFAULT_AUTH_TIMEOUT);    // TODO use a configurable wait time
-                return session;
-            } catch (Exception e) {
-                session.close(true);
-                throw e;
-            }
-        } catch (Exception e) {
-            client.close();
-            throw e;
-        }
-    }
-
-    public static Path resolveIdentityFile(String id) throws IOException {
-        BuiltinIdentities identity = BuiltinIdentities.fromName(id);
-        if (identity != null) {
-            String fileName = ClientIdentity.getIdentityFileName(identity.getName());
-            Path keysFolder = PublicKeyEntry.getDefaultKeysFolderPath();
-            return keysFolder.resolve(fileName);
-        } else {
-            return Paths.get(id);
-        }
-    }
-
-    // returns null if error encountered
-    public static SshClient setupClient(
-            Map<String, Object> options,
-            List<NamedFactory<Cipher>> ciphers,
-            List<NamedFactory<Mac>> macs,
-            List<NamedFactory<Compression>> compressions,
-            Collection<? extends Path> identities,
-            BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
-        if (GenericUtils.isEmpty(ciphers)) {
-            ciphers = setupCiphers(resolver, stderr);
-            if (ciphers == null) {
-                return null;
-            }
-        }
-
-        if (GenericUtils.isEmpty(macs)) {
-            macs = setupMacs(resolver, stderr);
-            if (macs == null) {
-                return null;
-            }
-        }
-
-        if (GenericUtils.isEmpty(compressions)) {
-            compressions = setupCompressions(resolver, stderr);
-            if (compressions == null) {
-                return null;
-            }
-        }
-
-        SshClient client = SshClient.setUpDefaultClient();
-        try {
-            if (GenericUtils.size(ciphers) > 0) {
-                client.setCipherFactories(ciphers);
-            }
-
-            if (GenericUtils.size(macs) > 0) {
-                client.setMacFactories(macs);
-            }
-
-            if (GenericUtils.size(compressions) > 0) {
-                client.setCompressionFactories(compressions);
-            }
-
-            try {
-                setupSessionIdentities(client, identities, stdin, stdout, stderr);
-            } catch (Throwable t) { // show but do not fail the setup - maybe a password can be used
-                showError(stderr, t.getClass().getSimpleName() + " while loading user keys: " + t.getMessage());
-            }
-
-            setupServerKeyVerifier(client, options, stdin, stdout, stderr);
-            setupSessionUserInteraction(client, stdin, stdout, stderr);
-
-            Map<String, Object> props = client.getProperties();
-            props.putAll(options);
-            return client;
-        } catch (Throwable t) {
-            showError(stderr, "Failed (" + t.getClass().getSimpleName() + ") to setup client: " + t.getMessage());
-            client.close();
-            return null;
-        }
-    }
-
-    public static FileKeyPairProvider setupSessionIdentities(ClientFactoryManager client, Collection<? extends Path> identities,
-            BufferedReader stdin, PrintStream stdout, PrintStream stderr)
-                throws Throwable {
-        client.setFilePasswordProvider(file -> {
-            stdout.print("Enter password for private key file=" + file + ": ");
-            return stdin.readLine();
-        });
-
-        if (GenericUtils.isEmpty(identities)) {
-            return null;
-        }
-
-        FileKeyPairProvider provider = new FileKeyPairProvider() {
-            @Override
-            public String toString() {
-                return FileKeyPairProvider.class.getSimpleName() + "[clientIdentitiesProvider]";
-            }
-        };
-        provider.setPaths(identities);
-        client.setKeyPairProvider(provider);
-        return provider;
-    }
-
-    public static UserInteraction setupSessionUserInteraction(
-            ClientAuthenticationManager client, BufferedReader stdin, PrintStream stdout, PrintStream stderr) {
-        UserInteraction ui = new UserInteraction() {
-            @Override
-            public boolean isInteractionAllowed(ClientSession session) {
-                return true;
-            }
-
-            @Override
-            public void serverVersionInfo(ClientSession session, List<String> lines) {
-                for (String l : lines) {
-                    stdout.append('\t').println(l);
-                }
-            }
-
-            @Override
-            public void welcome(ClientSession clientSession, String banner, String lang) {
-                stdout.println(banner);
-            }
-
-            @Override
-            public String[] interactive(ClientSession clientSession, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
-                int numPropmts = GenericUtils.length(prompt);
-                String[] answers = new String[numPropmts];
-                try {
-                    for (int i = 0; i < numPropmts; i++) {
-                        stdout.append(prompt[i]).print(" ");
-                        answers[i] = stdin.readLine();
-                    }
-                } catch (IOException e) {
-                    stderr.append(e.getClass().getSimpleName()).append(" while read prompts: ").println(e.getMessage());
-                }
-                return answers;
-            }
-
-            @Override
-            public String getUpdatedPassword(ClientSession clientSession, String prompt, String lang) {
-                stdout.append(prompt).print(" ");
-                try {
-                    return stdin.readLine();
-                } catch (IOException e) {
-                    stderr.append(e.getClass().getSimpleName()).append(" while read password: ").println(e.getMessage());
-                    return null;
-                }
-            }
-        };
-        client.setUserInteraction(ui);
-        return ui;
-    }
-
-    public static ServerKeyVerifier setupServerKeyVerifier(
-            ClientAuthenticationManager manager, Map<String, ?> options, BufferedReader stdin, PrintStream stdout, PrintStream stderr) {
-        ServerKeyVerifier current = manager.getServerKeyVerifier();
-        if (current == null) {
-            current = ClientBuilder.DEFAULT_SERVER_KEY_VERIFIER;
-            manager.setServerKeyVerifier(current);
-        }
-
-        String strictValue = Objects.toString(options.remove(KnownHostsServerKeyVerifier.STRICT_CHECKING_OPTION), "true");
-        if (!SshConfigFileReader.parseBooleanValue(strictValue)) {
-            return current;
-        }
-
-        String filePath = Objects.toString(options.remove(KnownHostsServerKeyVerifier.KNOWN_HOSTS_FILE_OPTION), null);
-        if (GenericUtils.isEmpty(filePath)) {
-            current = new DefaultKnownHostsServerKeyVerifier(current);
-        } else {    // if user specifies a different location than default be lenient
-            current = new DefaultKnownHostsServerKeyVerifier(current, false, Paths.get(filePath));
-        }
-
-        ((KnownHostsServerKeyVerifier) current).setModifiedServerKeyAcceptor((clientSession, remoteAddress, entry, expected, actual) -> {
-            stderr.append("Mismatched keys presented by ").append(Objects.toString(remoteAddress))
-                  .append(" for entry=").println(entry);
-            stderr.append('\t').append("Expected=").append(KeyUtils.getKeyType(expected))
-                  .append('-').println(KeyUtils.getFingerPrint(expected));
-            stderr.append('\t').append("Actual=").append(KeyUtils.getKeyType(actual))
-                  .append('-').println(KeyUtils.getFingerPrint(actual));
-            stderr.flush(); // just making sure
-
-            stdout.append("Accept key and update known hosts: y/[N]");
-            stdout.flush(); // just making sure
-
-            String ans = GenericUtils.trimToEmpty(stdin.readLine());
-            return (GenericUtils.length(ans) > 0) && (Character.toLowerCase(ans.charAt(0)) == 'y');
-        });
-
-        manager.setServerKeyVerifier(current);
-        return current;
-    }
-
-    public static Level resolveLoggingVerbosity(String... args) {
-        return resolveLoggingVerbosity(args, GenericUtils.length(args));
-    }
-
-    public static Level resolveLoggingVerbosity(String[] args, int maxIndex) {
-        for (int index = 0; index < maxIndex; index++) {
-            String argName = args[index];
-            if ("-v".equals(argName)) {
-                return Level.INFO;
-            } else if ("-vv".equals(argName)) {
-                return Level.FINE;
-            } else if ("-vvv".equals(argName)) {
-                return Level.FINEST;
-            }
-        }
-
-        return Level.WARNING;
-    }
-
-    public static OutputStream resolveLoggingTargetStream(PrintStream stdout, PrintStream stderr, String... args) {
-        return resolveLoggingTargetStream(stdout, stderr, args, GenericUtils.length(args));
-    }
-
-    public static OutputStream resolveLoggingTargetStream(PrintStream stdout, PrintStream stderr, String[] args, int maxIndex) {
-        for (int index = 0; index < maxIndex; index++) {
-            String argName = args[index];
-            if ("-E".equals(argName)) {
-                if ((index + 1) >= maxIndex) {
-                    showError(stderr, "Missing " + argName + " option argument");
-                    return null;
-                }
-
-                String argVal = args[index + 1];
-                if ("--".equals(argVal)) {
-                    return stdout;
-                }
-
-                try {
-                    Path path = Paths.get(argVal).normalize().toAbsolutePath();
-                    return Files.newOutputStream(path);
-                } catch (IOException e) {
-                    showError(stderr, "Failed (" + e.getClass().getSimpleName() + ") to open " + argVal + ": " + e.getMessage());
-                    return null;
-                }
-            }
-        }
-
-        return stderr;
-    }
-
-    public static List<NamedFactory<Compression>> setupCompressions(PropertyResolver options, PrintStream stderr) {
-        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.COMPRESSION_PROP);
-        if (GenericUtils.isEmpty(argVal)) {
-            return Collections.emptyList();
-        }
-
-        NamedFactory<Compression> value = CompressionConfigValue.fromName(argVal);
-        if (value == null) {
-            showError(stderr, "Unknown compression configuration value: " + argVal);
-            return null;
-        }
-
-        return Collections.singletonList(value);
-    }
-
-    public static List<NamedFactory<Compression>> setupCompressions(
-            String argName, String argVal, List<NamedFactory<Compression>> current, PrintStream stderr) {
-        if (GenericUtils.size(current) > 0) {
-            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
-            return null;
-        }
-
-        BuiltinCompressions.ParseResult result = BuiltinCompressions.parseCompressionsList(argVal);
-        Collection<? extends NamedFactory<Compression>> available = result.getParsedFactories();
-        if (GenericUtils.isEmpty(available)) {
-            showError(stderr, "No known compressions in " + argVal);
-            return null;
-        }
-
-        Collection<String> unsupported = result.getUnsupportedFactories();
-        if (GenericUtils.size(unsupported) > 0) {
-            stderr.append("Ignored unsupported compressions: ").println(GenericUtils.join(unsupported, ','));
-        }
-
-        return new ArrayList<>(available);
-    }
-
-    public static List<NamedFactory<Mac>> setupMacs(PropertyResolver options, PrintStream stderr) {
-        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.MACS_CONFIG_PROP);
-        return GenericUtils.isEmpty(argVal)
-             ? Collections.emptyList()
-             : setupMacs(SshConfigFileReader.MACS_CONFIG_PROP, argVal, null, stderr);
-    }
-
-    public static List<NamedFactory<Mac>> setupMacs(String argName, String argVal, List<NamedFactory<Mac>> current, PrintStream stderr) {
-        if (GenericUtils.size(current) > 0) {
-            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
-            return null;
-        }
-
-        BuiltinMacs.ParseResult result = BuiltinMacs.parseMacsList(argVal);
-        Collection<? extends NamedFactory<Mac>> available = result.getParsedFactories();
-        if (GenericUtils.isEmpty(available)) {
-            showError(stderr, "No known MACs in " + argVal);
-            return null;
-        }
-
-        Collection<String> unsupported = result.getUnsupportedFactories();
-        if (GenericUtils.size(unsupported) > 0) {
-            stderr.append("Ignored unsupported MACs: ").println(GenericUtils.join(unsupported, ','));
-        }
-
-        return new ArrayList<>(available);
-    }
-
-    public static List<NamedFactory<Cipher>> setupCiphers(PropertyResolver options, PrintStream stderr) {
-        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.CIPHERS_CONFIG_PROP);
-        return GenericUtils.isEmpty(argVal)
-             ? Collections.emptyList()
-             : setupCiphers(SshConfigFileReader.CIPHERS_CONFIG_PROP, argVal, null, stderr);
-    }
-
-    // returns null - e.g., re-specified or no supported cipher found
-    public static List<NamedFactory<Cipher>> setupCiphers(String argName, String argVal, List<NamedFactory<Cipher>> current, PrintStream stderr) {
-        if (GenericUtils.size(current) > 0) {
-            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
-            return null;
-        }
-
-        BuiltinCiphers.ParseResult result = BuiltinCiphers.parseCiphersList(argVal);
-        Collection<? extends NamedFactory<Cipher>> available = result.getParsedFactories();
-        if (GenericUtils.isEmpty(available)) {
-            showError(stderr, "No known ciphers in " + argVal);
-            return null;
-        }
-
-        Collection<String> unsupported = result.getUnsupportedFactories();
-        if (GenericUtils.size(unsupported) > 0) {
-            stderr.append("Ignored unsupported ciphers: ").println(GenericUtils.join(unsupported, ','));
-        }
-
-        return new ArrayList<>(available);
-    }
-
-    public static Handler setupLogging(Level level, final PrintStream stdout, final PrintStream stderr, final OutputStream outputStream) {
-        Handler fh = new ConsoleHandler() {
-            {
-                setOutputStream(outputStream); // override the default (stderr)
-            }
-
-            @Override
-            protected synchronized void setOutputStream(OutputStream out) throws SecurityException {
-                if ((out == stdout) || (out == stderr)) {
-                    super.setOutputStream(new NoCloseOutputStream(out));
-                } else {
-                    super.setOutputStream(out);
-                }
-            }
-        };
-        fh.setLevel(Level.FINEST);
-        fh.setFormatter(new Formatter() {
-            @Override
-            public String format(LogRecord record) {
-                String message = formatMessage(record);
-                String throwable = "";
-                Throwable t = record.getThrown();
-                if (t != null) {
-                    StringWriter sw = new StringWriter();
-                    try (PrintWriter pw = new PrintWriter(sw)) {
-                        pw.println();
-                        t.printStackTrace(pw);  // NOPMD
-                    }
-                    throwable = sw.toString();
-                }
-                return String.format("%1$tY-%1$tm-%1$td: %2$-7.7s: %3$-32.32s: %4$s%5$s%n",
-                        new Date(record.getMillis()), record.getLevel().getName(),
-                        record.getLoggerName(), message, throwable);
-            }
-        });
-
-        Logger root = Logger.getLogger("");
-        for (Handler handler : root.getHandlers()) {
-            root.removeHandler(handler);
-        }
-        root.addHandler(fh);
-        root.setLevel(level);
-        return fh;
-    }
-
-    //////////////////////////////////////////////////////////////////////////
-
-    public static void main(String[] args) throws Exception {
-        PrintStream stdout = System.out;
-        PrintStream stderr = System.err;
-        boolean agentForward = false;
-        List<String> command = null;
-        int socksPort = -1;
-        int numArgs = GenericUtils.length(args);
-        boolean error = false;
-        String target = null;
-        Level level = Level.WARNING;
-        OutputStream logStream = stderr;
-        for (int i = 0; i < numArgs; i++) {
-            String argName = args[i];
-            // handled by 'setupClientSession'
-            if (GenericUtils.isEmpty(command) && isArgumentedOption("-p", argName)) {
-                if ((i + 1) >= numArgs) {
-                    error = showError(stderr, "option requires an argument: " + argName);
-                    break;
-                }
-
-                i++;
-                continue;
-            }
-
-            // verbosity handled separately
-            if (GenericUtils.isEmpty(command) && ("-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName))) {
-                continue;
-            }
-
-            if (GenericUtils.isEmpty(command) && "-D".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    error = showError(stderr, "option requires an argument: " + argName);
-                    break;
-                }
-                if (socksPort > 0) {
-                    error = showError(stderr, argName + " option value re-specified: " + socksPort);
-                    break;
-                }
-
-                socksPort = Integer.parseInt(args[++i]);
-                if (socksPort <= 0) {
-                    error = showError(stderr, "Bad option value for " + argName + ": " + socksPort);
-                    break;
-                }
-            } else if (GenericUtils.isEmpty(command) && "-A".equals(argName)) {
-                agentForward = true;
-            } else if (GenericUtils.isEmpty(command) && "-a".equals(argName)) {
-                agentForward = false;
-            } else {
-                level = resolveLoggingVerbosity(args, i);
-                logStream = resolveLoggingTargetStream(stdout, stderr, args, i);
-                if (logStream == null) {
-                    error = true;
-                    break;
-                }
-                if (GenericUtils.isEmpty(command) && target == null) {
-                    target = argName;
-                } else {
-                    if (command == null) {
-                        command = new ArrayList<>();
-                    }
-                    command.add(argName);
-                }
-            }
-        }
-
-        ClientSession session = null;
-        try (BufferedReader stdin = new BufferedReader(
-                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
-            if (!error) {
-                setupLogging(level, stdout, stderr, logStream);
-
-                session = setupClientSession(SSH_CLIENT_PORT_OPTION, stdin, stdout, stderr, args);
-                if (session == null) {
-                    error = true;
-                }
-            }
-
-            if (error) {
-                System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-E logoutputfile] [-D socksPort]"
-                        + " [-l login] [" + SSH_CLIENT_PORT_OPTION + " port] [-o option=value]"
-                        + " [-w password] [-c cipherslist] [-m maclist] [-C]"
-                        + " hostname/user@host [command]");
-                System.exit(-1);
-                return;
-            }
-
-            try (SshClient client = (SshClient) session.getFactoryManager()) {
-                /*
-                String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
-                if (authSock == null && provider != null) {
-                    Iterable<KeyPair> keys = provider.loadKeys();
-                    AgentServer server = new AgentServer();
-                    authSock = server.start();
-                    SshAgent agent = new AgentClient(authSock);
-                    for (KeyPair key : keys) {
-                        agent.addIdentity(key, "");
-                    }
-                    agent.close();
-                    props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
-                }
-                */
-
-                try {
-                    if (socksPort >= 0) {
-                        session.startDynamicPortForwarding(new SshdSocketAddress(SshdSocketAddress.LOCALHOST_NAME, socksPort));
-                        Thread.sleep(Long.MAX_VALUE);
-                    } else {
-                        ClientChannel channel;
-                        if (GenericUtils.isEmpty(command)) {
-                            channel = session.createShellChannel();
-                            ((ChannelShell) channel).setAgentForwarding(agentForward);
-                            channel.setIn(new NoCloseInputStream(System.in));
-                        } else {
-                            channel = session.createExecChannel(String.join(" ", command).trim());
-                        }
-
-                        try (OutputStream channelOut = new NoCloseOutputStream(System.out);
-                             OutputStream channelErr =  new NoCloseOutputStream(System.err)) {
-                            channel.setOut(channelOut);
-                            channel.setErr(channelErr);
-                            channel.open().await(); // TODO use verify and a configurable timeout
-                            channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
-                        } finally {
-                            channel.close();
-                        }
-                        session.close(false);
-                    }
-                } finally {
-                    client.stop();
-                }
-            } finally {
-                session.close();
-            }
-        } finally {
-            if (logStream != null && logStream != stdout && logStream != stderr) {
-                logStream.close();
-            }
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java b/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
deleted file mode 100644
index 6aa7dbb..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
+++ /dev/null
@@ -1,740 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.InterruptedIOException;
-import java.io.OutputStreamWriter;
-import java.io.PrintStream;
-import java.io.Writer;
-import java.net.ConnectException;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.nio.channels.Channel;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.Level;
-
-import org.apache.sshd.client.auth.keyboard.UserInteraction;
-import org.apache.sshd.client.future.ConnectFuture;
-import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.cipher.ECCurves;
-import org.apache.sshd.common.config.SshConfigFileReader;
-import org.apache.sshd.common.config.keys.BuiltinIdentities;
-import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.kex.KexProposalOption;
-import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionListener;
-import org.apache.sshd.common.signature.BuiltinSignatures;
-import org.apache.sshd.common.signature.Signature;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.common.util.logging.LoggingUtils;
-import org.apache.sshd.common.util.logging.SimplifiedLog;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.apache.sshd.common.util.security.SecurityUtils;
-
-/**
- * A naive implementation of <A HREF="https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1">ssh-keyscan(1)</A>
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class SshKeyScan implements Channel, Callable<Void>, ServerKeyVerifier, SessionListener, SimplifiedLog {
-    /**
-     * Default key types if not overridden from the command line
-     */
-    public static final List<String> DEFAULT_KEY_TYPES =
-            Collections.unmodifiableList(Arrays.asList(BuiltinIdentities.Constants.RSA, BuiltinIdentities.Constants.ECDSA));
-    public static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
-    public static final Level DEFAULT_LEVEL = Level.INFO;
-
-    private final AtomicBoolean open = new AtomicBoolean(true);
-    private SshClient client;
-    private int port;
-    private long timeout;
-    private List<String> keyTypes;
-    private InputStream input;
-    private Level level;
-    private final Map<String, String> currentHostFingerprints = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
-    public SshKeyScan() {
-        super();
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public void setPort(int port) {
-        this.port = port;
-    }
-
-    public InputStream getInputStream() {
-        return input;
-    }
-
-    public void setInputStream(InputStream input) {
-        this.input = input;
-    }
-
-    public List<String> getKeyTypes() {
-        return keyTypes;
-    }
-
-    public void setKeyTypes(List<String> keyTypes) {
-        this.keyTypes = keyTypes;
-    }
-
-    public long getTimeout() {
-        return timeout;
-    }
-
-    public void setTimeout(long timeout) {
-        this.timeout = timeout;
-    }
-
-    public Level getLogLevel() {
-        return level;
-    }
-
-    public void setLogLevel(Level level) {
-        this.level = level;
-    }
-
-    @Override
-    public void log(Level level, Object message, Throwable t) {
-        if (isEnabled(level)) {
-            PrintStream ps = System.out;
-            if ((t != null) || Level.SEVERE.equals(level) || Level.WARNING.equals(level)) {
-                ps = System.err;
-            }
-
-            ps.append('\t').println(message);
-            if (t != null) {
-                ps.append("\t\t").append(t.getClass().getSimpleName()).append(": ").println(t.getMessage());
-            }
-        }
-    }
-
-    @Override
-    public boolean isEnabled(Level level) {
-        return LoggingUtils.isLoggable(level, getLogLevel());
-    }
-
-    @Override
-    public Void call() throws Exception {
-        ValidateUtils.checkTrue(isOpen(), "Scanner is closed");
-
-        Collection<String> typeNames = getKeyTypes();
-        Map<String, List<KeyPair>> pairsMap = createKeyPairs(typeNames);
-        /*
-         * We will need to switch signature factories for each specific
-         * key type in order to force the server to send ONLY that specific
-         * key, so pre-create the factories map according to the selected
-         * key types
-         */
-        SortedMap<String, List<NamedFactory<Signature>>> sigFactories = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        Collection<String> sigTypes = GenericUtils.asSortedSet(sigFactories.comparator(), pairsMap.keySet());
-        for (String kt : sigTypes) {
-            List<NamedFactory<Signature>> factories = resolveSignatureFactories(kt);
-            if (GenericUtils.isEmpty(factories)) {
-                if (isEnabled(Level.FINEST)) {
-                    log(Level.FINEST, "Skip empty signature factories for " + kt);
-                }
-                pairsMap.remove(kt);
-            } else {
-                sigFactories.put(kt, factories);
-            }
-        }
-
-        ValidateUtils.checkTrue(!GenericUtils.isEmpty(pairsMap), "No client key pairs");
-        ValidateUtils.checkTrue(!GenericUtils.isEmpty(sigFactories), "No signature factories");
-
-        Exception err = null;
-        try {
-            ValidateUtils.checkTrue(client == null, "Client still active");
-            client = SshClient.setUpDefaultClient();
-            client.setServerKeyVerifier(this);
-
-            try (BufferedReader rdr = new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) {
-                client.setUserInteraction(new UserInteraction() {
-                    @Override
-                    public boolean isInteractionAllowed(ClientSession session) {
-                        return true;
-                    }
-
-                    @Override
-                    public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
-                        return null;
-                    }
-
-                    @Override
-                    public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
-                        return null;
-                    }
-
-                    @Override
-                    public void serverVersionInfo(ClientSession session, List<String> lines) {
-                        if (isEnabled(Level.FINE) && GenericUtils.isNotEmpty(lines)) {
-                            for (String l : lines) {
-                                log(Level.FINE, "Server Info: " + l);
-                            }
-                        }
-                    }
-
-                    @Override
-                    public void welcome(ClientSession session, String banner, String lang) {
-                        if (isEnabled(Level.FINE) && GenericUtils.isNotEmpty(banner)) {
-                            String[] lines = GenericUtils.split(banner, '\n');
-                            for (String l : lines) {
-                                log(Level.FINE, "Welcome[" + lang + "]: " + l);
-                            }
-                        }
-                    }
-                });
-
-                client.start();
-                for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
-                    line = GenericUtils.replaceWhitespaceAndTrim(line);
-
-                    String[] hosts = GenericUtils.split(line, ',');
-                    if (GenericUtils.isEmpty(hosts)) {
-                        continue;
-                    }
-
-                    for (String h : hosts) {
-                        if (!isOpen()) {
-                            throw new InterruptedIOException("Closed while preparing to contact host=" + h);
-                        }
-
-                        try {
-                            resolveServerKeys(client, h, pairsMap, sigFactories);
-                        } catch (Exception e) {
-                            // check if interrupted while scanning host keys
-                            if (e instanceof InterruptedIOException) {
-                                throw e;
-                            }
-
-                            if (isEnabled(Level.FINE)) {
-                                log(Level.FINE, "Failed to retrieve keys from " + h, e);
-                            }
-                            err = GenericUtils.accumulateException(err, e);
-                        } finally {
-                            currentHostFingerprints.clear();
-                        }
-                    }
-                }
-            }
-        } finally {
-            try {
-                close();
-            } catch (IOException e) {
-                err = GenericUtils.accumulateException(err, e);
-            }
-        }
-
-        if (err != null) {
-            throw err;
-        }
-
-        return null;
-    }
-
-    protected void resolveServerKeys(SshClient client, String host,
-            Map<String, List<KeyPair>> pairsMap, Map<String, List<NamedFactory<Signature>>> sigFactories)
-                    throws IOException {
-        // Cannot use forEach because of the potential for throwing IOException by the invoked code
-        for (Map.Entry<String, List<KeyPair>> pe : pairsMap.entrySet()) {
-            String kt = pe.getKey();
-            if (!isOpen()) {
-                throw new InterruptedIOException("Closed while attempting to retrieve key type=" + kt + " from " + host);
-            }
-
-            List<NamedFactory<Signature>> current = client.getSignatureFactories();
-            try {
-                /*
-                 * Replace whatever factories there are right now with the
-                 * specific one for the key in order to extract only the
-                 * specific host key type
-                 */
-                List<NamedFactory<Signature>> forced = sigFactories.get(kt);
-                client.setSignatureFactories(forced);
-                resolveServerKeys(client, host, kt, pe.getValue());
-            } catch (Exception e) {
-                if (isEnabled(Level.FINE)) {
-                    log(Level.FINE, "Failed to resolve key=" + kt + " for " + host);
-                }
-
-                if (e instanceof ConnectException) {
-                    return; // makes no sense to try again with another key type...
-                }
-            } finally {
-                client.setSignatureFactories(current);  // don't have to, but be nice...
-            }
-        }
-    }
-
-    protected void resolveServerKeys(SshClient client, String host, String kt, List<KeyPair> ids) throws Exception {
-        int connectPort = getPort();
-        if (isEnabled(Level.FINE)) {
-            log(Level.FINE, "Connecting to " + host + ":" + connectPort + " to retrieve key type=" + kt);
-        }
-
-        ConnectFuture future = client.connect(UUID.randomUUID().toString(), host, connectPort);
-        long waitTime = getTimeout();
-        if (!future.await(waitTime)) {
-            throw new ConnectException("Failed to connect to " + host + ":" + connectPort
-                    + " within " + waitTime + " msec."
-                    + " to retrieve key type=" + kt);
-        }
-
-        try (ClientSession session = future.getSession()) {
-            IoSession ioSession = session.getIoSession();
-            SocketAddress remoteAddress = ioSession.getRemoteAddress();
-            String remoteLocation = toString(remoteAddress);
-            if (isEnabled(Level.FINE)) {
-                log(Level.FINE, "Connected to " + remoteLocation + " to retrieve key type=" + kt);
-            }
-
-            try {
-                session.addSessionListener(this);
-                if (isEnabled(Level.FINER)) {
-                    log(Level.FINER, "Authenticating with key type=" + kt + " to " + remoteLocation);
-                }
-
-                GenericUtils.forEach(ids, session::addPublicKeyIdentity);
-
-                try {
-                    // shouldn't really succeed, but do it since key exchange occurs only on auth attempt
-                    session.auth().verify(waitTime);
-                    log(Level.WARNING, "Unexpected authentication success using key type=" + kt + " with " + remoteLocation);
-                } catch (Exception e) {
-                    if (isEnabled(Level.FINER)) {
-                        log(Level.FINER, "Failed to authenticate using key type=" + kt + " with " + remoteLocation);
-                    }
-                } finally {
-                    GenericUtils.forEach(ids, session::removePublicKeyIdentity);
-                }
-            } finally {
-                session.removeSessionListener(this);
-            }
-        }
-    }
-
-    @Override
-    public void sessionCreated(Session session) {
-        logSessionEvent(session, "Created");
-    }
-
-    @Override
-    public void sessionEvent(Session session, Event event) {
-        logSessionEvent(session, event);
-        if (isEnabled(Level.FINEST) && Event.KexCompleted.equals(event)) {
-            IoSession ioSession = session.getIoSession();
-            SocketAddress remoteAddress = ioSession.getRemoteAddress();
-            String remoteLocation = toString(remoteAddress);
-            for (KexProposalOption paramType : KexProposalOption.VALUES) {
-                String paramValue = session.getNegotiatedKexParameter(paramType);
-                log(Level.FINEST, remoteLocation + "[" + paramType.getDescription() + "]: " + paramValue);
-            }
-        }
-    }
-
-    @Override
-    public void sessionException(Session session, Throwable t) {
-        logSessionEvent(session, t);
-    }
-
-    @Override
-    public void sessionClosed(Session session) {
-        logSessionEvent(session, "Closed");
-    }
-
-    @Override
-    public void sessionNegotiationStart(
-            Session session, Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal) {
-        logSessionEvent(session, "sessionNegotiationStart");
-        logNegotiationProposal("c2s", clientProposal);
-        logNegotiationProposal("s2c", serverProposal);
-    }
-
-    protected void logNegotiationProposal(String type, Map<KexProposalOption, String> proposal) {
-        if (!isEnabled(Level.FINEST)) {
-            return;
-        }
-
-        proposal.forEach((option, value) -> log(Level.FINEST, option.getDescription() + "[" + type + "]: " + value));
-    }
-
-    @Override
-    public void sessionNegotiationEnd(Session session, Map<KexProposalOption, String> clientProposal,
-            Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
-            Throwable reason) {
-        if (reason == null) {
-            logSessionEvent(session, "sessionNegotiationStart");
-        } else {
-            logSessionEvent(session, reason);
-        }
-    }
-
-    protected void logSessionEvent(Session session, Object event) {
-        if (isEnabled(Level.FINEST)) {
-            IoSession ioSession = session.getIoSession();
-            SocketAddress remoteAddress = ioSession.getRemoteAddress();
-            log(Level.FINEST, "Session " + toString(remoteAddress) + " event: " + event);
-        }
-    }
-
-    @Override
-    public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) {
-        String remoteLocation = toString(remoteAddress);
-        String extra = KeyUtils.getFingerPrint(serverKey);
-        try {
-            String keyType = KeyUtils.getKeyType(serverKey);
-            String current = GenericUtils.isEmpty(keyType) ? null : currentHostFingerprints.get(keyType);
-            if (Objects.equals(current, extra)) {
-                if (isEnabled(Level.FINER)) {
-                    log(Level.FINER, "verifyServerKey(" + remoteLocation + ")[" + keyType + "] skip existing key: " + extra);
-                }
-            } else {
-                if (isEnabled(Level.FINE)) {
-                    log(Level.FINE, "verifyServerKey(" + remoteLocation + ")[" + keyType + "] found new key: " + extra);
-                }
-
-                writeServerKey(remoteLocation, keyType, serverKey);
-
-                if (!GenericUtils.isEmpty(keyType)) {
-                    currentHostFingerprints.put(keyType, extra);
-                }
-            }
-        } catch (Exception e) {
-            log(Level.SEVERE, "Failed to output the public key " + extra + " from " + remoteLocation, e);
-        }
-
-        return true;
-    }
-
-    protected void writeServerKey(String remoteLocation, String keyType, PublicKey serverKey) throws Exception {
-        StringBuilder sb = new StringBuilder(256).append(remoteLocation).append(' ');
-        PublicKeyEntry.appendPublicKeyEntry(sb, serverKey);
-        log(Level.INFO, sb);
-    }
-
-    private static String toString(SocketAddress addr) {
-        if (addr == null) {
-            return null;
-        } else if (addr instanceof InetSocketAddress) {
-            return ((InetSocketAddress) addr).getHostString();
-        } else if (addr instanceof SshdSocketAddress) {
-            return ((SshdSocketAddress) addr).getHostName();
-        } else {
-            return addr.toString();
-        }
-    }
-
-    protected List<NamedFactory<Signature>> resolveSignatureFactories(String keyType) throws GeneralSecurityException {
-        if (isEnabled(Level.FINE)) {
-            log(Level.FINE, "Resolve signature factories for " + keyType);
-        }
-
-        if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
-            return Collections.singletonList(BuiltinSignatures.rsa);
-        } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
-            return Collections.singletonList(BuiltinSignatures.dsa);
-        } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
-            List<NamedFactory<Signature>> factories = new ArrayList<>(ECCurves.NAMES.size());
-            for (String n : ECCurves.NAMES) {
-                if (isEnabled(Level.FINER)) {
-                    log(Level.FINER, "Resolve signature factory for curve=" + n);
-                }
-
-                NamedFactory<Signature> f =
-                        ValidateUtils.checkNotNull(BuiltinSignatures.fromString(n), "Unknown curve signature: %s", n);
-                factories.add(f);
-            }
-
-            return factories;
-        } else if (BuiltinIdentities.Constants.ED25519.equalsIgnoreCase(keyType)) {
-            return Collections.singletonList(BuiltinSignatures.ed25519);
-        } else {
-            throw new NoSuchAlgorithmException("Unknown key type: " + keyType);
-        }
-    }
-
-    protected Map<String, List<KeyPair>> createKeyPairs(Collection<String> typeNames) throws GeneralSecurityException {
-        if (GenericUtils.isEmpty(typeNames)) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, List<KeyPair>> pairsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        for (String kt : typeNames) {
-            if ("*".equalsIgnoreCase(kt) || "all".equalsIgnoreCase(kt)) {
-                ValidateUtils.checkTrue(typeNames.size() == 1, "Wildcard key type must be the only one specified: %s", typeNames);
-                return createKeyPairs(BuiltinIdentities.NAMES);
-            }
-
-            if (pairsMap.containsKey(kt)) {
-                log(Level.WARNING, "Key type " + kt + " re-specified");
-                continue;
-            }
-
-            List<KeyPair> kps = createKeyPairs(kt);
-            if (GenericUtils.isEmpty(kps)) {
-                log(Level.WARNING, "No key-pairs generated for key type " + kt);
-                continue;
-            }
-
-            pairsMap.put(kt, kps);
-        }
-
-        return pairsMap;
-    }
-
-    protected List<KeyPair> createKeyPairs(String keyType) throws GeneralSecurityException {
-        if (isEnabled(Level.FINE)) {
-            log(Level.FINE, "Generate key pairs for " + keyType);
-        }
-
-        if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
-            return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_RSA, 1024));
-        } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
-            return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_DSS, 512));
-        } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
-            if (!SecurityUtils.isECCSupported()) {
-                throw new NoSuchAlgorithmException("ECC not supported: " + keyType);
-            }
-
-            List<KeyPair> kps = new ArrayList<>(ECCurves.NAMES.size());
-            for (ECCurves curve : ECCurves.VALUES) {
-                String curveName = curve.getName();
-                if (isEnabled(Level.FINER)) {
-                    log(Level.FINER, "Generate key pair for curve=" + curveName);
-                }
-
-                kps.add(KeyUtils.generateKeyPair(curve.getKeyType(), curve.getKeySize()));
-            }
-
-            return kps;
-        } else if (BuiltinIdentities.Constants.ED25519.equalsIgnoreCase(keyType)) {
-            if (!SecurityUtils.isEDDSACurveSupported()) {
-                throw new NoSuchAlgorithmException("EDDSA curves not supported: " + keyType);
-            }
-
-            KeyPairGenerator g = SecurityUtils.getKeyPairGenerator(SecurityUtils.EDDSA);
-            return Collections.singletonList(g.generateKeyPair());
-        } else {
-            throw new InvalidKeySpecException("Unknown key type: " + keyType);
-        }
-    }
-
-    @Override
-    public boolean isOpen() {
-        return open.get();
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (!open.getAndSet(false)) {
-            return; // already closed
-        }
-
-        IOException err = null;
-        if (input != null) {
-            try {
-                input.close();
-            } catch (IOException e) {
-                err = GenericUtils.accumulateException(err, e);
-            } finally {
-                input = null;
-            }
-        }
-
-        if (client != null) {
-            try {
-                client.close();
-            } catch (IOException e) {
-                err = GenericUtils.accumulateException(err, e);
-            } finally {
-                try {
-                    client.stop();
-                } finally {
-                    client = null;
-                }
-            }
-        }
-        if (err != null) {
-            throw err;
-        }
-    }
-
-    //////////////////////////////////////////////////////////////////////////
-
-    // returns a List of the hosts to be contacted
-    public static List<String> parseCommandLineArguments(SshKeyScan scanner, String... args) throws IOException {
-        int numArgs = GenericUtils.length(args);
-        for (int index = 0; index < numArgs; index++) {
-            String optName = args[index];
-            if ("-f".equals(optName)) {
-                index++;
-                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
-                ValidateUtils.checkTrue(scanner.getInputStream() == null, "%s option re-specified", optName);
-
-                String filePath = args[index];
-                if ("-".equals(filePath)) {
-                    scanner.setInputStream(new NoCloseInputStream(System.in));
-                } else {
-                    scanner.setInputStream(new FileInputStream(filePath));
-                }
-            } else if ("-t".equals(optName)) {
-                index++;
-                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
-                ValidateUtils.checkTrue(GenericUtils.isEmpty(scanner.getKeyTypes()), "%s option re-specified", optName);
-
-                String typeList = args[index];
-                String[] types = GenericUtils.split(typeList, ',');
-                ValidateUtils.checkTrue(GenericUtils.length(types) > 0, "No types specified for %s", optName);
-                scanner.setKeyTypes(Arrays.asList(types));
-            } else if ("-p".equals(optName)) {
-                index++;
-                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
-                ValidateUtils.checkTrue(scanner.getPort() <= 0, "%s option re-specified", optName);
-
-                String portValue = args[index];
-                int port = Integer.parseInt(portValue);
-                ValidateUtils.checkTrue((port > 0) && (port <= 0xFFFF), "Bad port: %s", portValue);
-                scanner.setPort(port);
-            } else if ("-T".equals(optName)) {
-                index++;
-                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
-                ValidateUtils.checkTrue(scanner.getTimeout() <= 0, "%s option re-specified", optName);
-
-                String timeoutValue = args[index];
-                long timeout = Long.parseLong(timeoutValue);
-                ValidateUtils.checkTrue(timeout > 0L, "Bad timeout: %s", timeoutValue);
-                scanner.setTimeout(timeout);
-            } else if ("-v".equals(optName)) {
-                ValidateUtils.checkTrue(scanner.getLogLevel() == null, "%s option re-specified", optName);
-                scanner.setLogLevel(Level.FINEST);
-            } else {    // stop at first non-option - assume the rest are host names/addresses
-                ValidateUtils.checkTrue(optName.charAt(0) != '-', "Unknown option: %s", optName);
-
-                int remaining = numArgs - index;
-                if (remaining == 1) {
-                    return Collections.singletonList(optName);
-                }
-
-                List<String> hosts = new ArrayList<>(remaining);
-                for (; index < numArgs; index++) {
-                    hosts.add(args[index]);
-                }
-
-                return hosts;
-            }
-        }
-
-        return Collections.emptyList();
-    }
-
-    /* -------------------------------------------------------------------- */
-
-    public static <S extends SshKeyScan> S setInputStream(S scanner, Collection<String> hosts) throws IOException {
-        if (GenericUtils.isEmpty(hosts)) {
-            Objects.requireNonNull(scanner.getInputStream(), "No hosts or file specified");
-        } else {
-            ValidateUtils.checkTrue(scanner.getInputStream() == null, "Both hosts and file specified");
-
-            // convert the hosts from the arguments into a "file" - one host per line
-            try (ByteArrayOutputStream baos = new ByteArrayOutputStream(hosts.size() * 32)) {
-                try (Writer w = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) {
-                    for (String h : hosts) {
-                        w.append(h).append(IoUtils.EOL);
-                    }
-                }
-
-                byte[] data = baos.toByteArray();
-                scanner.setInputStream(new ByteArrayInputStream(data));
-            }
-        }
-
-        return scanner;
-    }
-
-    public static <S extends SshKeyScan> S initializeScanner(S scanner, Collection<String> hosts) throws IOException {
-        setInputStream(scanner, hosts);
-        if (scanner.getPort() <= 0) {
-            scanner.setPort(SshConfigFileReader.DEFAULT_PORT);
-        }
-
-        if (scanner.getTimeout() <= 0L) {
-            scanner.setTimeout(DEFAULT_TIMEOUT);
-        }
-
-        if (GenericUtils.isEmpty(scanner.getKeyTypes())) {
-            scanner.setKeyTypes(DEFAULT_KEY_TYPES);
-        }
-
-        if (scanner.getLogLevel() == null) {
-            scanner.setLogLevel(DEFAULT_LEVEL);
-        }
-
-        return scanner;
-    }
-
-    /* -------------------------------------------------------------------- */
-
-    public static void main(String[] args) throws Exception {
-        try (SshKeyScan scanner = new SshKeyScan()) {
-            Collection<String> hosts = parseCommandLineArguments(scanner, args);
-            initializeScanner(scanner, hosts);
-            scanner.call();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
index dfc7f7d..16d0cb2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -18,26 +18,17 @@
  */
 package org.apache.sshd.client.scp;
 
-import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.PrintStream;
-import java.nio.charset.Charset;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
 import java.nio.file.attribute.PosixFilePermission;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.logging.Level;
 
-import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.FactoryManager;
@@ -46,23 +37,15 @@ import org.apache.sshd.common.file.util.MockFileSystem;
 import org.apache.sshd.common.file.util.MockPath;
 import org.apache.sshd.common.scp.ScpFileOpener;
 import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpLocation;
 import org.apache.sshd.common.scp.ScpTimestamp;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class DefaultScpClient extends AbstractScpClient {
-    /**
-     * Command line option used to indicate a non-default port
-     */
-    public static final String SCP_PORT_OPTION = "-P";
-
     protected final ScpFileOpener opener;
     protected final ScpTransferEventListener listener;
     private final ClientSession clientSession;
@@ -172,172 +155,5 @@ public class DefaultScpClient extends AbstractScpClient {
             channel.close(false);
         }
     }
-
-    //////////////////////////////////////////////////////////////////////////
-
-    private static boolean showError(PrintStream stderr, String message) {
-        stderr.println(message);
-        return true;
-    }
-
-    private static String[] normalizeCommandArguments(PrintStream stdout, PrintStream stderr, String... args) {
-        int numArgs = GenericUtils.length(args);
-        if (numArgs <= 0) {
-            return args;
-        }
-
-        List<String> effective = new ArrayList<>(numArgs);
-        boolean error = false;
-        for (int index = 0; (index < numArgs) && (!error); index++) {
-            String argName = args[index];
-            // handled by 'setupClientSession'
-            if (SshClient.isArgumentedOption(SCP_PORT_OPTION, argName)) {
-                if ((index + 1) >= numArgs) {
-                    error = showError(stderr, "option requires an argument: " + argName);
-                    break;
-                }
-
-                effective.add(argName);
-                effective.add(args[++index]);
-            } else if ("-r".equals(argName) || "-p".equals(argName)
-                    || "-q".equals(argName) || "-C".equals(argName)
-                    || "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) {
-                effective.add(argName);
-            } else if (argName.charAt(0) == '-') {
-                error = showError(stderr, "Unknown option: " + argName);
-                break;
-            } else {
-                if ((index + 1) >= numArgs) {
-                    error = showError(stderr, "Not enough arguments");
-                    break;
-                }
-
-                ScpLocation source = new ScpLocation(argName);
-                ScpLocation target = new ScpLocation(args[++index]);
-                if (index < (numArgs - 1)) {
-                    error = showError(stderr, "Unexpected extra arguments");
-                    break;
-                }
-
-                if (source.isLocal() == target.isLocal()) {
-                    error = showError(stderr, "Both targets are either remote or local");
-                    break;
-                }
-
-                ScpLocation remote = source.isLocal() ? target : source;
-                effective.add(remote.resolveUsername() + "@" + remote.getHost());
-                effective.add(source.toString());
-                effective.add(target.toString());
-                break;
-            }
-        }
-
-        if (error) {
-            return null;
-        }
-
-        return effective.toArray(new String[effective.size()]);
-    }
-
-    public static void main(String[] args) throws Exception {
-        final PrintStream stdout = System.out;
-        final PrintStream stderr = System.err;
-        OutputStream logStream = stdout;
-        try (BufferedReader stdin = new BufferedReader(
-                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
-            args = normalizeCommandArguments(stdout, stderr, args);
-            int numArgs = GenericUtils.length(args);
-            // see the way normalizeCommandArguments works...
-            if (numArgs >= 2) {
-                Level level = SshClient.resolveLoggingVerbosity(args, numArgs - 2);
-                logStream = SshClient.resolveLoggingTargetStream(stdout, stderr, args, numArgs - 2);
-                if (logStream != null) {
-                    SshClient.setupLogging(level, stdout, stderr, logStream);
-                }
-            }
-
-            ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
-                    ? null : SshClient.setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
-            if (session == null) {
-                stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity]"
-                         + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
-                         + " [-c cipherlist] [-m maclist] [-w password] [-C] <source> <target>");
-                stderr.println();
-                stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path");
-                stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local");
-                System.exit(-1);
-                return; // not that we really need it...
-            }
-
-            try {
-                // see the way normalizeCommandArguments works...
-                Collection<Option> options = EnumSet.noneOf(Option.class);
-                boolean quiet = false;
-                for (int index = 0; index < numArgs; index++) {
-                    String argName = args[index];
-                    if ("-r".equals(argName)) {
-                        options.add(Option.Recursive);
-                    } else if ("-p".equals(argName)) {
-                        options.add(Option.PreserveAttributes);
-                    } else if ("-q".equals(argName)) {
-                        quiet = true;
-                    }
-                }
-
-                if (!quiet) {
-                    session.setScpTransferEventListener(new ScpTransferEventListener() {
-                        @Override
-                        public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
-                            logEvent("startFolderEvent", op, file, -1L, perms, null);
-                        }
-
-                        @Override
-                        public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
-                            logEvent("endFolderEvent", op, file, -1L, perms, thrown);
-                        }
-
-                        @Override
-                        public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
-                            logEvent("startFileEvent", op, file, length, perms, null);
-                        }
-
-                        @Override
-                        public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
-                            logEvent("endFileEvent", op, file, length, perms, thrown);
-                        }
-
-                        private void logEvent(String name, FileOperation op, Path file, long length, Collection<PosixFilePermission> perms, Throwable thrown) {
-                            PrintStream ps = (thrown == null) ? stdout : stderr;
-                            ps.append('\t').append(name).append('[').append(op.name()).append(']').append(' ').append(file.toString());
-                            if (length > 0L) {
-                                ps.append(' ').append("length=").append(Long.toString(length));
-                            }
-                            ps.append(' ').append(String.valueOf(perms));
-
-                            if (thrown != null) {
-                                ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage());
-                            }
-                            ps.println();
-                        }
-                    });
-                }
-
-                ScpClient client = session.createScpClient();
-                ScpLocation source = new ScpLocation(args[numArgs - 2]);
-                ScpLocation target = new ScpLocation(args[numArgs - 1]);
-                if (source.isLocal()) {
-                    client.upload(source.getPath(), target.getPath(), options);
-                } else {
-                    client.download(source.getPath(), target.getPath(), options);
-                }
-            } finally {
-                session.close();
-            }
-        } finally {
-            if ((logStream != stdout) && (logStream != stderr)) {
-                logStream.close();
-            }
-        }
-    }
 }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java b/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
deleted file mode 100644
index 66f66dd..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client;
-
-/**
- * Just a test class used to invoke {@link SshClient#main(String[])} in
- * order to have logging - which is in {@code test} scope
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class SshClientMain {
-    private SshClientMain() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    public static void main(String[] args) throws Exception {
-        SshClient.main(args);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/test/java/org/apache/sshd/client/SshKeyScanMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/SshKeyScanMain.java b/sshd-core/src/test/java/org/apache/sshd/client/SshKeyScanMain.java
deleted file mode 100644
index 856b324..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/SshKeyScanMain.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client;
-
-/**
- * Just a test class used to invoke {@link SshKeyScan#main(String[])} in
- * order to have logging - which is in {@code test} scope
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class SshKeyScanMain {
-    private SshKeyScanMain() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    public static void main(String[] args) throws Exception {
-        SshKeyScan.main(args);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecMain.java b/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecMain.java
deleted file mode 100644
index 798fe70..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecMain.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client.channel;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.PrintStream;
-import java.nio.charset.Charset;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.util.test.BaseTestSupport;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ChannelExecMain extends BaseTestSupport {
-    public static void doExecuteCommands(
-            BufferedReader stdin, PrintStream stdout, PrintStream stderr, ClientSession session) throws Exception {
-        while (true) {
-            stdout.print("> ");
-
-            String command = stdin.readLine();
-            if ("q".equalsIgnoreCase(command) || "quit".equalsIgnoreCase(command)) {
-                break;
-            }
-            if (GenericUtils.isEmpty(command)) {
-                continue;
-            }
-
-            while (true) {
-                try {
-                    String response = session.executeRemoteCommand(command);
-                    String[] lines = GenericUtils.split(response, '\n');
-                    for (String l : lines) {
-                        stdout.append('\t').println(l);
-                    }
-                } catch (Exception e) {
-                    stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
-                }
-
-                stdout.append("Execute ").append(command).print(" again [y]/n ");
-                String ans = stdin.readLine();
-                if ((GenericUtils.length(ans) > 0) && (Character.toLowerCase(ans.charAt(0)) != 'y')) {
-                    break;
-                }
-            }
-        }
-    }
-
-    public static void main(String[] args) throws Exception {
-        PrintStream stdout = System.out;
-        PrintStream stderr = System.err;
-        try (BufferedReader stdin = new BufferedReader(
-                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
-            ClientSession session = SshClient.setupClientSession("-P", stdin, stdout, stderr, args);
-            if (session == null) {
-                System.err.println("usage: channelExec [-i identity] [-l login] [-P port] [-o option=value]"
-                        + " [-w password] [-c cipherlist]  [-m maclist] [-C] hostname/user@host");
-                System.exit(-1);
-                return;
-            }
-
-            try (SshClient client = (SshClient) session.getFactoryManager()) {
-                try (ClientSession clientSession = session) {
-                    doExecuteCommands(stdin, stdout, stderr, session);
-                } finally {
-                    client.stop();
-                }
-            }
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpCommandMain.java b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpCommandMain.java
deleted file mode 100644
index 75b0454..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpCommandMain.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client.scp;
-
-/**
- * Just a test class used to invoke {@link org.apache.sshd.client.scp.DefaultScpClient#main(String[])} in
- * order to have logging - which is in {@code test} scope
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class ScpCommandMain {
-    private ScpCommandMain() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    public static void main(String[] args) throws Exception {
-        DefaultScpClient.main(args);
-    }
-}


[5/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for client commands to sshd-cli module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
new file mode 100644
index 0000000..47021e1
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -0,0 +1,632 @@
+/*
+ * 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.
+ */
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.apache.sshd.cli.CliSupport;
+import org.apache.sshd.client.ClientAuthenticationManager;
+import org.apache.sshd.client.ClientBuilder;
+import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.keys.ClientIdentity;
+import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.cipher.BuiltinCiphers;
+import org.apache.sshd.common.cipher.Cipher;
+import org.apache.sshd.common.compression.BuiltinCompressions;
+import org.apache.sshd.common.compression.Compression;
+import org.apache.sshd.common.config.CompressionConfigValue;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.mac.BuiltinMacs;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class SshClientCliSupport extends CliSupport {
+    /**
+     * Command line option used to indicate non-default target port
+     */
+    public static final String SSH_CLIENT_PORT_OPTION = "-p";
+
+    protected SshClientCliSupport() {
+        super();
+    }
+
+    public static boolean isArgumentedOption(String portOption, String argName) {
+        return portOption.equals(argName)
+             || "-i".equals(argName)
+             || "-o".equals(argName)
+             || "-l".equals(argName)
+             || "-w".equals(argName)
+             || "-c".equals(argName)
+             || "-m".equals(argName)
+             || "-E".equals(argName);
+    }
+
+    // NOTE: ClientSession#getFactoryManager is the SshClient
+    public static ClientSession setupClientSession(
+            String portOption, BufferedReader stdin, PrintStream stdout, PrintStream stderr, String... args)
+                throws Exception {
+        int port = -1;
+        String host = null;
+        String login = null;
+        String password = null;
+        boolean error = false;
+        List<Path> identities = new ArrayList<>();
+        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        List<NamedFactory<Cipher>> ciphers = null;
+        List<NamedFactory<Mac>> macs = null;
+        List<NamedFactory<Compression>> compressions = null;
+        int numArgs = GenericUtils.length(args);
+        for (int i = 0; (!error) && (i < numArgs); i++) {
+            String argName = args[i];
+            String argVal = null;
+            if (isArgumentedOption(portOption, argName)) {
+                if ((i + 1) >= numArgs) {
+                    error = showError(stderr, "option requires an argument: " + argName);
+                    break;
+                }
+
+                argVal = args[++i];
+            }
+
+            if (portOption.equals(argName)) {
+                if (port > 0) {
+                    error = showError(stderr, argName + " option value re-specified: " + port);
+                    break;
+                }
+
+                port = Integer.parseInt(argVal);
+                if (port <= 0) {
+                    error = showError(stderr, "Bad option value for " + argName + ": " + port);
+                    break;
+                }
+            } else if ("-w".equals(argName)) {
+                if (GenericUtils.length(password) > 0) {
+                    error = showError(stderr, argName + " option value re-specified: " + password);
+                    break;
+                }
+                password = argVal;
+            } else if ("-c".equals(argName)) {
+                ciphers = setupCiphers(argName, argVal, ciphers, stderr);
+                if (GenericUtils.isEmpty(ciphers)) {
+                    error = true;
+                    break;
+                }
+            } else if ("-m".equals(argName)) {
+                macs = setupMacs(argName, argVal, macs, stderr);
+                if (GenericUtils.isEmpty(macs)) {
+                    error = true;
+                    break;
+                }
+            } else if ("-i".equals(argName)) {
+                identities.add(resolveIdentityFile(argVal));
+            } else if ("-C".equals(argName)) {
+                compressions = setupCompressions(argName,
+                    GenericUtils.join(
+                        Arrays.asList(
+                            BuiltinCompressions.Constants.ZLIB, BuiltinCompressions.Constants.DELAYED_ZLIB), ','),
+                    compressions, stderr);
+                if (GenericUtils.isEmpty(compressions)) {
+                    error = true;
+                    break;
+                }
+            } else if ("-o".equals(argName)) {
+                String opt = argVal;
+                int idx = opt.indexOf('=');
+                if (idx <= 0) {
+                    error = showError(stderr, "bad syntax for option: " + opt);
+                    break;
+                }
+
+                String optName = opt.substring(0, idx);
+                String optValue = opt.substring(idx + 1);
+                if (HostConfigEntry.IDENTITY_FILE_CONFIG_PROP.equals(optName)) {
+                    Path idFile = resolveIdentityFile(optValue);
+                    identities.add(idFile);
+                } else {
+                    options.put(optName, optValue);
+                }
+            } else if ("-l".equals(argName)) {
+                if (login != null) {
+                    error = showError(stderr, argName + " option value re-specified: " + port);
+                    break;
+                }
+
+                login = argVal;
+            } else if (argName.charAt(0) != '-') {
+                if (host != null) { // assume part of a command following it
+                    break;
+                }
+
+                host = argName;
+                int pos = host.indexOf('@');  // check if user@host
+                if (pos > 0) {
+                    if (login == null) {
+                        login = host.substring(0, pos);
+                        host = host.substring(pos + 1);
+                    } else {
+                        error = showError(stderr, "Login already specified using -l option (" + login + "): " + host);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if ((!error) && GenericUtils.isEmpty(host)) {
+            error = showError(stderr, "Hostname not specified");
+        }
+
+        if (error) {
+            return null;
+        }
+
+        SshClient client = setupClient(options, ciphers, macs, compressions, identities, stdin, stdout, stderr);
+        if (client == null) {
+            return null;
+        }
+
+        try {
+            client.start();
+
+            if (login == null) {
+                login = OsUtils.getCurrentUser();
+            }
+
+            if (port <= 0) {
+                port = SshConfigFileReader.DEFAULT_PORT;
+            }
+
+            // TODO use a configurable wait time
+            ClientSession session = client.connect(login, host, port).verify().getSession();
+            try {
+                if (GenericUtils.length(password) > 0) {
+                    session.addPasswordIdentity(password);
+                }
+                session.auth().verify(FactoryManager.DEFAULT_AUTH_TIMEOUT);    // TODO use a configurable wait time
+                return session;
+            } catch (Exception e) {
+                session.close(true);
+                throw e;
+            }
+        } catch (Exception e) {
+            client.close();
+            throw e;
+        }
+    }
+
+    public static Path resolveIdentityFile(String id) throws IOException {
+        BuiltinIdentities identity = BuiltinIdentities.fromName(id);
+        if (identity != null) {
+            String fileName = ClientIdentity.getIdentityFileName(identity.getName());
+            Path keysFolder = PublicKeyEntry.getDefaultKeysFolderPath();
+            return keysFolder.resolve(fileName);
+        } else {
+            return Paths.get(id);
+        }
+    }
+
+    // returns null if error encountered
+    public static SshClient setupClient(
+            Map<String, Object> options,
+            List<NamedFactory<Cipher>> ciphers,
+            List<NamedFactory<Mac>> macs,
+            List<NamedFactory<Compression>> compressions,
+            Collection<? extends Path> identities,
+            BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
+        if (GenericUtils.isEmpty(ciphers)) {
+            ciphers = setupCiphers(resolver, stderr);
+            if (ciphers == null) {
+                return null;
+            }
+        }
+
+        if (GenericUtils.isEmpty(macs)) {
+            macs = setupMacs(resolver, stderr);
+            if (macs == null) {
+                return null;
+            }
+        }
+
+        if (GenericUtils.isEmpty(compressions)) {
+            compressions = setupCompressions(resolver, stderr);
+            if (compressions == null) {
+                return null;
+            }
+        }
+
+        SshClient client = SshClient.setUpDefaultClient();
+        try {
+            if (GenericUtils.size(ciphers) > 0) {
+                client.setCipherFactories(ciphers);
+            }
+
+            if (GenericUtils.size(macs) > 0) {
+                client.setMacFactories(macs);
+            }
+
+            if (GenericUtils.size(compressions) > 0) {
+                client.setCompressionFactories(compressions);
+            }
+
+            try {
+                setupSessionIdentities(client, identities, stdin, stdout, stderr);
+            } catch (Throwable t) { // show but do not fail the setup - maybe a password can be used
+                showError(stderr, t.getClass().getSimpleName() + " while loading user keys: " + t.getMessage());
+            }
+
+            setupServerKeyVerifier(client, options, stdin, stdout, stderr);
+            setupSessionUserInteraction(client, stdin, stdout, stderr);
+
+            Map<String, Object> props = client.getProperties();
+            props.putAll(options);
+            return client;
+        } catch (Throwable t) {
+            showError(stderr, "Failed (" + t.getClass().getSimpleName() + ") to setup client: " + t.getMessage());
+            client.close();
+            return null;
+        }
+    }
+
+    public static FileKeyPairProvider setupSessionIdentities(ClientFactoryManager client, Collection<? extends Path> identities,
+            BufferedReader stdin, PrintStream stdout, PrintStream stderr)
+                throws Throwable {
+        client.setFilePasswordProvider(file -> {
+            stdout.print("Enter password for private key file=" + file + ": ");
+            return stdin.readLine();
+        });
+
+        if (GenericUtils.isEmpty(identities)) {
+            return null;
+        }
+
+        FileKeyPairProvider provider = new FileKeyPairProvider() {
+            @Override
+            public String toString() {
+                return FileKeyPairProvider.class.getSimpleName() + "[clientIdentitiesProvider]";
+            }
+        };
+        provider.setPaths(identities);
+        client.setKeyPairProvider(provider);
+        return provider;
+    }
+
+    public static UserInteraction setupSessionUserInteraction(
+            ClientAuthenticationManager client, BufferedReader stdin, PrintStream stdout, PrintStream stderr) {
+        UserInteraction ui = new UserInteraction() {
+            @Override
+            public boolean isInteractionAllowed(ClientSession session) {
+                return true;
+            }
+
+            @Override
+            public void serverVersionInfo(ClientSession session, List<String> lines) {
+                for (String l : lines) {
+                    stdout.append('\t').println(l);
+                }
+            }
+
+            @Override
+            public void welcome(ClientSession clientSession, String banner, String lang) {
+                stdout.println(banner);
+            }
+
+            @Override
+            public String[] interactive(ClientSession clientSession, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+                int numPropmts = GenericUtils.length(prompt);
+                String[] answers = new String[numPropmts];
+                try {
+                    for (int i = 0; i < numPropmts; i++) {
+                        stdout.append(prompt[i]).print(" ");
+                        answers[i] = stdin.readLine();
+                    }
+                } catch (IOException e) {
+                    stderr.append(e.getClass().getSimpleName()).append(" while read prompts: ").println(e.getMessage());
+                }
+                return answers;
+            }
+
+            @Override
+            public String getUpdatedPassword(ClientSession clientSession, String prompt, String lang) {
+                stdout.append(prompt).print(" ");
+                try {
+                    return stdin.readLine();
+                } catch (IOException e) {
+                    stderr.append(e.getClass().getSimpleName()).append(" while read password: ").println(e.getMessage());
+                    return null;
+                }
+            }
+        };
+        client.setUserInteraction(ui);
+        return ui;
+    }
+
+    public static ServerKeyVerifier setupServerKeyVerifier(
+            ClientAuthenticationManager manager, Map<String, ?> options, BufferedReader stdin, PrintStream stdout, PrintStream stderr) {
+        ServerKeyVerifier current = manager.getServerKeyVerifier();
+        if (current == null) {
+            current = ClientBuilder.DEFAULT_SERVER_KEY_VERIFIER;
+            manager.setServerKeyVerifier(current);
+        }
+
+        String strictValue = Objects.toString(options.remove(KnownHostsServerKeyVerifier.STRICT_CHECKING_OPTION), "true");
+        if (!SshConfigFileReader.parseBooleanValue(strictValue)) {
+            return current;
+        }
+
+        String filePath = Objects.toString(options.remove(KnownHostsServerKeyVerifier.KNOWN_HOSTS_FILE_OPTION), null);
+        if (GenericUtils.isEmpty(filePath)) {
+            current = new DefaultKnownHostsServerKeyVerifier(current);
+        } else {    // if user specifies a different location than default be lenient
+            current = new DefaultKnownHostsServerKeyVerifier(current, false, Paths.get(filePath));
+        }
+
+        ((KnownHostsServerKeyVerifier) current).setModifiedServerKeyAcceptor((clientSession, remoteAddress, entry, expected, actual) -> {
+            stderr.append("Mismatched keys presented by ").append(Objects.toString(remoteAddress))
+                  .append(" for entry=").println(entry);
+            stderr.append('\t').append("Expected=").append(KeyUtils.getKeyType(expected))
+                  .append('-').println(KeyUtils.getFingerPrint(expected));
+            stderr.append('\t').append("Actual=").append(KeyUtils.getKeyType(actual))
+                  .append('-').println(KeyUtils.getFingerPrint(actual));
+            stderr.flush(); // just making sure
+
+            stdout.append("Accept key and update known hosts: y/[N]");
+            stdout.flush(); // just making sure
+
+            String ans = GenericUtils.trimToEmpty(stdin.readLine());
+            return (GenericUtils.length(ans) > 0) && (Character.toLowerCase(ans.charAt(0)) == 'y');
+        });
+
+        manager.setServerKeyVerifier(current);
+        return current;
+    }
+
+    public static Level resolveLoggingVerbosity(String... args) {
+        return resolveLoggingVerbosity(args, GenericUtils.length(args));
+    }
+
+    public static Level resolveLoggingVerbosity(String[] args, int maxIndex) {
+        for (int index = 0; index < maxIndex; index++) {
+            String argName = args[index];
+            if ("-v".equals(argName)) {
+                return Level.INFO;
+            } else if ("-vv".equals(argName)) {
+                return Level.FINE;
+            } else if ("-vvv".equals(argName)) {
+                return Level.FINEST;
+            }
+        }
+
+        return Level.WARNING;
+    }
+
+    public static OutputStream resolveLoggingTargetStream(PrintStream stdout, PrintStream stderr, String... args) {
+        return resolveLoggingTargetStream(stdout, stderr, args, GenericUtils.length(args));
+    }
+
+    public static OutputStream resolveLoggingTargetStream(PrintStream stdout, PrintStream stderr, String[] args, int maxIndex) {
+        for (int index = 0; index < maxIndex; index++) {
+            String argName = args[index];
+            if ("-E".equals(argName)) {
+                if ((index + 1) >= maxIndex) {
+                    showError(stderr, "Missing " + argName + " option argument");
+                    return null;
+                }
+
+                String argVal = args[index + 1];
+                if ("--".equals(argVal)) {
+                    return stdout;
+                }
+
+                try {
+                    Path path = Paths.get(argVal).normalize().toAbsolutePath();
+                    return Files.newOutputStream(path);
+                } catch (IOException e) {
+                    showError(stderr, "Failed (" + e.getClass().getSimpleName() + ") to open " + argVal + ": " + e.getMessage());
+                    return null;
+                }
+            }
+        }
+
+        return stderr;
+    }
+
+    public static List<NamedFactory<Compression>> setupCompressions(PropertyResolver options, PrintStream stderr) {
+        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.COMPRESSION_PROP);
+        if (GenericUtils.isEmpty(argVal)) {
+            return Collections.emptyList();
+        }
+
+        NamedFactory<Compression> value = CompressionConfigValue.fromName(argVal);
+        if (value == null) {
+            showError(stderr, "Unknown compression configuration value: " + argVal);
+            return null;
+        }
+
+        return Collections.singletonList(value);
+    }
+
+    public static List<NamedFactory<Compression>> setupCompressions(
+            String argName, String argVal, List<NamedFactory<Compression>> current, PrintStream stderr) {
+        if (GenericUtils.size(current) > 0) {
+            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
+            return null;
+        }
+
+        BuiltinCompressions.ParseResult result = BuiltinCompressions.parseCompressionsList(argVal);
+        Collection<? extends NamedFactory<Compression>> available = result.getParsedFactories();
+        if (GenericUtils.isEmpty(available)) {
+            showError(stderr, "No known compressions in " + argVal);
+            return null;
+        }
+
+        Collection<String> unsupported = result.getUnsupportedFactories();
+        if (GenericUtils.size(unsupported) > 0) {
+            stderr.append("Ignored unsupported compressions: ").println(GenericUtils.join(unsupported, ','));
+        }
+
+        return new ArrayList<>(available);
+    }
+
+    public static List<NamedFactory<Mac>> setupMacs(PropertyResolver options, PrintStream stderr) {
+        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.MACS_CONFIG_PROP);
+        return GenericUtils.isEmpty(argVal)
+             ? Collections.emptyList()
+             : setupMacs(SshConfigFileReader.MACS_CONFIG_PROP, argVal, null, stderr);
+    }
+
+    public static List<NamedFactory<Mac>> setupMacs(String argName, String argVal, List<NamedFactory<Mac>> current, PrintStream stderr) {
+        if (GenericUtils.size(current) > 0) {
+            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
+            return null;
+        }
+
+        BuiltinMacs.ParseResult result = BuiltinMacs.parseMacsList(argVal);
+        Collection<? extends NamedFactory<Mac>> available = result.getParsedFactories();
+        if (GenericUtils.isEmpty(available)) {
+            showError(stderr, "No known MACs in " + argVal);
+            return null;
+        }
+
+        Collection<String> unsupported = result.getUnsupportedFactories();
+        if (GenericUtils.size(unsupported) > 0) {
+            stderr.append("Ignored unsupported MACs: ").println(GenericUtils.join(unsupported, ','));
+        }
+
+        return new ArrayList<>(available);
+    }
+
+    public static List<NamedFactory<Cipher>> setupCiphers(PropertyResolver options, PrintStream stderr) {
+        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.CIPHERS_CONFIG_PROP);
+        return GenericUtils.isEmpty(argVal)
+             ? Collections.emptyList()
+             : setupCiphers(SshConfigFileReader.CIPHERS_CONFIG_PROP, argVal, null, stderr);
+    }
+
+    // returns null - e.g., re-specified or no supported cipher found
+    public static List<NamedFactory<Cipher>> setupCiphers(String argName, String argVal, List<NamedFactory<Cipher>> current, PrintStream stderr) {
+        if (GenericUtils.size(current) > 0) {
+            showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
+            return null;
+        }
+
+        BuiltinCiphers.ParseResult result = BuiltinCiphers.parseCiphersList(argVal);
+        Collection<? extends NamedFactory<Cipher>> available = result.getParsedFactories();
+        if (GenericUtils.isEmpty(available)) {
+            showError(stderr, "No known ciphers in " + argVal);
+            return null;
+        }
+
+        Collection<String> unsupported = result.getUnsupportedFactories();
+        if (GenericUtils.size(unsupported) > 0) {
+            stderr.append("Ignored unsupported ciphers: ").println(GenericUtils.join(unsupported, ','));
+        }
+
+        return new ArrayList<>(available);
+    }
+
+    public static Handler setupLogging(Level level, final PrintStream stdout, final PrintStream stderr, final OutputStream outputStream) {
+        Handler fh = new ConsoleHandler() {
+            {
+                setOutputStream(outputStream); // override the default (stderr)
+            }
+
+            @Override
+            protected synchronized void setOutputStream(OutputStream out) throws SecurityException {
+                if ((out == stdout) || (out == stderr)) {
+                    super.setOutputStream(new NoCloseOutputStream(out));
+                } else {
+                    super.setOutputStream(out);
+                }
+            }
+        };
+        fh.setLevel(Level.FINEST);
+        fh.setFormatter(new Formatter() {
+            @Override
+            public String format(LogRecord record) {
+                String message = formatMessage(record);
+                String throwable = "";
+                Throwable t = record.getThrown();
+                if (t != null) {
+                    StringWriter sw = new StringWriter();
+                    try (PrintWriter pw = new PrintWriter(sw)) {
+                        pw.println();
+                        t.printStackTrace(pw);  // NOPMD
+                    }
+                    throwable = sw.toString();
+                }
+                return String.format("%1$tY-%1$tm-%1$td: %2$-7.7s: %3$-32.32s: %4$s%5$s%n",
+                        new Date(record.getMillis()), record.getLevel().getName(),
+                        record.getLoggerName(), message, throwable);
+            }
+        });
+
+        Logger root = Logger.getLogger("");
+        for (Handler handler : root.getHandlers()) {
+            root.removeHandler(handler);
+        }
+        root.addHandler(fh);
+        root.setLevel(level);
+        return fh;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
new file mode 100644
index 0000000..9ebca2c
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ChannelShell;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SshClientMain extends SshClientCliSupport {
+    protected SshClientMain() {
+        super();    // in case someone wants to extend it
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static void main(String[] args) throws Exception {
+        PrintStream stdout = System.out;
+        PrintStream stderr = System.err;
+        boolean agentForward = false;
+        List<String> command = null;
+        int socksPort = -1;
+        int numArgs = GenericUtils.length(args);
+        boolean error = false;
+        String target = null;
+        Level level = Level.WARNING;
+        OutputStream logStream = stderr;
+        for (int i = 0; i < numArgs; i++) {
+            String argName = args[i];
+            // handled by 'setupClientSession'
+            if (GenericUtils.isEmpty(command) && isArgumentedOption("-p", argName)) {
+                if ((i + 1) >= numArgs) {
+                    error = showError(stderr, "option requires an argument: " + argName);
+                    break;
+                }
+
+                i++;
+                continue;
+            }
+
+            // verbosity handled separately
+            if (GenericUtils.isEmpty(command) && ("-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName))) {
+                continue;
+            }
+
+            if (GenericUtils.isEmpty(command) && "-D".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    error = showError(stderr, "option requires an argument: " + argName);
+                    break;
+                }
+                if (socksPort > 0) {
+                    error = showError(stderr, argName + " option value re-specified: " + socksPort);
+                    break;
+                }
+
+                socksPort = Integer.parseInt(args[++i]);
+                if (socksPort <= 0) {
+                    error = showError(stderr, "Bad option value for " + argName + ": " + socksPort);
+                    break;
+                }
+            } else if (GenericUtils.isEmpty(command) && "-A".equals(argName)) {
+                agentForward = true;
+            } else if (GenericUtils.isEmpty(command) && "-a".equals(argName)) {
+                agentForward = false;
+            } else {
+                level = resolveLoggingVerbosity(args, i);
+                logStream = resolveLoggingTargetStream(stdout, stderr, args, i);
+                if (logStream == null) {
+                    error = true;
+                    break;
+                }
+                if (GenericUtils.isEmpty(command) && target == null) {
+                    target = argName;
+                } else {
+                    if (command == null) {
+                        command = new ArrayList<>();
+                    }
+                    command.add(argName);
+                }
+            }
+        }
+
+        ClientSession session = null;
+        try (BufferedReader stdin = new BufferedReader(
+                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
+            if (!error) {
+                setupLogging(level, stdout, stderr, logStream);
+
+                session = setupClientSession(SSH_CLIENT_PORT_OPTION, stdin, stdout, stderr, args);
+                if (session == null) {
+                    error = true;
+                }
+            }
+
+            if (error) {
+                System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-E logoutputfile] [-D socksPort]"
+                        + " [-l login] [" + SSH_CLIENT_PORT_OPTION + " port] [-o option=value]"
+                        + " [-w password] [-c cipherslist] [-m maclist] [-C]"
+                        + " hostname/user@host [command]");
+                System.exit(-1);
+                return;
+            }
+
+            try (SshClient client = (SshClient) session.getFactoryManager()) {
+                /*
+                String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
+                if (authSock == null && provider != null) {
+                    Iterable<KeyPair> keys = provider.loadKeys();
+                    AgentServer server = new AgentServer();
+                    authSock = server.start();
+                    SshAgent agent = new AgentClient(authSock);
+                    for (KeyPair key : keys) {
+                        agent.addIdentity(key, "");
+                    }
+                    agent.close();
+                    props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
+                }
+                */
+
+                try {
+                    if (socksPort >= 0) {
+                        session.startDynamicPortForwarding(new SshdSocketAddress(SshdSocketAddress.LOCALHOST_NAME, socksPort));
+                        Thread.sleep(Long.MAX_VALUE);
+                    } else {
+                        ClientChannel channel;
+                        if (GenericUtils.isEmpty(command)) {
+                            channel = session.createShellChannel();
+                            ((ChannelShell) channel).setAgentForwarding(agentForward);
+                            channel.setIn(new NoCloseInputStream(System.in));
+                        } else {
+                            channel = session.createExecChannel(String.join(" ", command).trim());
+                        }
+
+                        try (OutputStream channelOut = new NoCloseOutputStream(System.out);
+                             OutputStream channelErr =  new NoCloseOutputStream(System.err)) {
+                            channel.setOut(channelOut);
+                            channel.setErr(channelErr);
+                            channel.open().await(); // TODO use verify and a configurable timeout
+                            channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
+                        } finally {
+                            channel.close();
+                        }
+                        session.close(false);
+                    }
+                } finally {
+                    client.stop();
+                }
+            } finally {
+                session.close();
+            }
+        } finally {
+            if (logStream != null && logStream != stdout && logStream != stderr) {
+                logStream.close();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshKeyScanMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshKeyScanMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshKeyScanMain.java
new file mode 100644
index 0000000..b5350f0
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshKeyScanMain.java
@@ -0,0 +1,741 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Writer;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.Channel;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.logging.LoggingUtils;
+import org.apache.sshd.common.util.logging.SimplifiedLog;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * A naive implementation of <A HREF="https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1">ssh-keyscan(1)</A>
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SshKeyScanMain implements Channel, Callable<Void>, ServerKeyVerifier, SessionListener, SimplifiedLog {
+    /**
+     * Default key types if not overridden from the command line
+     */
+    public static final List<String> DEFAULT_KEY_TYPES =
+            Collections.unmodifiableList(Arrays.asList(BuiltinIdentities.Constants.RSA, BuiltinIdentities.Constants.ECDSA));
+    public static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+    public static final Level DEFAULT_LEVEL = Level.INFO;
+
+    private final AtomicBoolean open = new AtomicBoolean(true);
+    private SshClient client;
+    private int port;
+    private long timeout;
+    private List<String> keyTypes;
+    private InputStream input;
+    private Level level;
+    private final Map<String, String> currentHostFingerprints = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+    public SshKeyScanMain() {
+        super();
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public InputStream getInputStream() {
+        return input;
+    }
+
+    public void setInputStream(InputStream input) {
+        this.input = input;
+    }
+
+    public List<String> getKeyTypes() {
+        return keyTypes;
+    }
+
+    public void setKeyTypes(List<String> keyTypes) {
+        this.keyTypes = keyTypes;
+    }
+
+    public long getTimeout() {
+        return timeout;
+    }
+
+    public void setTimeout(long timeout) {
+        this.timeout = timeout;
+    }
+
+    public Level getLogLevel() {
+        return level;
+    }
+
+    public void setLogLevel(Level level) {
+        this.level = level;
+    }
+
+    @Override
+    public void log(Level level, Object message, Throwable t) {
+        if (isEnabled(level)) {
+            PrintStream ps = System.out;
+            if ((t != null) || Level.SEVERE.equals(level) || Level.WARNING.equals(level)) {
+                ps = System.err;
+            }
+
+            ps.append('\t').println(message);
+            if (t != null) {
+                ps.append("\t\t").append(t.getClass().getSimpleName()).append(": ").println(t.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public boolean isEnabled(Level level) {
+        return LoggingUtils.isLoggable(level, getLogLevel());
+    }
+
+    @Override
+    public Void call() throws Exception {
+        ValidateUtils.checkTrue(isOpen(), "Scanner is closed");
+
+        Collection<String> typeNames = getKeyTypes();
+        Map<String, List<KeyPair>> pairsMap = createKeyPairs(typeNames);
+        /*
+         * We will need to switch signature factories for each specific
+         * key type in order to force the server to send ONLY that specific
+         * key, so pre-create the factories map according to the selected
+         * key types
+         */
+        SortedMap<String, List<NamedFactory<Signature>>> sigFactories = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        Collection<String> sigTypes = GenericUtils.asSortedSet(sigFactories.comparator(), pairsMap.keySet());
+        for (String kt : sigTypes) {
+            List<NamedFactory<Signature>> factories = resolveSignatureFactories(kt);
+            if (GenericUtils.isEmpty(factories)) {
+                if (isEnabled(Level.FINEST)) {
+                    log(Level.FINEST, "Skip empty signature factories for " + kt);
+                }
+                pairsMap.remove(kt);
+            } else {
+                sigFactories.put(kt, factories);
+            }
+        }
+
+        ValidateUtils.checkTrue(!GenericUtils.isEmpty(pairsMap), "No client key pairs");
+        ValidateUtils.checkTrue(!GenericUtils.isEmpty(sigFactories), "No signature factories");
+
+        Exception err = null;
+        try {
+            ValidateUtils.checkTrue(client == null, "Client still active");
+            client = SshClient.setUpDefaultClient();
+            client.setServerKeyVerifier(this);
+
+            try (BufferedReader rdr = new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) {
+                client.setUserInteraction(new UserInteraction() {
+                    @Override
+                    public boolean isInteractionAllowed(ClientSession session) {
+                        return true;
+                    }
+
+                    @Override
+                    public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+                        return null;
+                    }
+
+                    @Override
+                    public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                        return null;
+                    }
+
+                    @Override
+                    public void serverVersionInfo(ClientSession session, List<String> lines) {
+                        if (isEnabled(Level.FINE) && GenericUtils.isNotEmpty(lines)) {
+                            for (String l : lines) {
+                                log(Level.FINE, "Server Info: " + l);
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void welcome(ClientSession session, String banner, String lang) {
+                        if (isEnabled(Level.FINE) && GenericUtils.isNotEmpty(banner)) {
+                            String[] lines = GenericUtils.split(banner, '\n');
+                            for (String l : lines) {
+                                log(Level.FINE, "Welcome[" + lang + "]: " + l);
+                            }
+                        }
+                    }
+                });
+
+                client.start();
+                for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+                    line = GenericUtils.replaceWhitespaceAndTrim(line);
+
+                    String[] hosts = GenericUtils.split(line, ',');
+                    if (GenericUtils.isEmpty(hosts)) {
+                        continue;
+                    }
+
+                    for (String h : hosts) {
+                        if (!isOpen()) {
+                            throw new InterruptedIOException("Closed while preparing to contact host=" + h);
+                        }
+
+                        try {
+                            resolveServerKeys(client, h, pairsMap, sigFactories);
+                        } catch (Exception e) {
+                            // check if interrupted while scanning host keys
+                            if (e instanceof InterruptedIOException) {
+                                throw e;
+                            }
+
+                            if (isEnabled(Level.FINE)) {
+                                log(Level.FINE, "Failed to retrieve keys from " + h, e);
+                            }
+                            err = GenericUtils.accumulateException(err, e);
+                        } finally {
+                            currentHostFingerprints.clear();
+                        }
+                    }
+                }
+            }
+        } finally {
+            try {
+                close();
+            } catch (IOException e) {
+                err = GenericUtils.accumulateException(err, e);
+            }
+        }
+
+        if (err != null) {
+            throw err;
+        }
+
+        return null;
+    }
+
+    protected void resolveServerKeys(SshClient client, String host,
+            Map<String, List<KeyPair>> pairsMap, Map<String, List<NamedFactory<Signature>>> sigFactories)
+                    throws IOException {
+        // Cannot use forEach because of the potential for throwing IOException by the invoked code
+        for (Map.Entry<String, List<KeyPair>> pe : pairsMap.entrySet()) {
+            String kt = pe.getKey();
+            if (!isOpen()) {
+                throw new InterruptedIOException("Closed while attempting to retrieve key type=" + kt + " from " + host);
+            }
+
+            List<NamedFactory<Signature>> current = client.getSignatureFactories();
+            try {
+                /*
+                 * Replace whatever factories there are right now with the
+                 * specific one for the key in order to extract only the
+                 * specific host key type
+                 */
+                List<NamedFactory<Signature>> forced = sigFactories.get(kt);
+                client.setSignatureFactories(forced);
+                resolveServerKeys(client, host, kt, pe.getValue());
+            } catch (Exception e) {
+                if (isEnabled(Level.FINE)) {
+                    log(Level.FINE, "Failed to resolve key=" + kt + " for " + host);
+                }
+
+                if (e instanceof ConnectException) {
+                    return; // makes no sense to try again with another key type...
+                }
+            } finally {
+                client.setSignatureFactories(current);  // don't have to, but be nice...
+            }
+        }
+    }
+
+    protected void resolveServerKeys(SshClient client, String host, String kt, List<KeyPair> ids) throws Exception {
+        int connectPort = getPort();
+        if (isEnabled(Level.FINE)) {
+            log(Level.FINE, "Connecting to " + host + ":" + connectPort + " to retrieve key type=" + kt);
+        }
+
+        ConnectFuture future = client.connect(UUID.randomUUID().toString(), host, connectPort);
+        long waitTime = getTimeout();
+        if (!future.await(waitTime)) {
+            throw new ConnectException("Failed to connect to " + host + ":" + connectPort
+                    + " within " + waitTime + " msec."
+                    + " to retrieve key type=" + kt);
+        }
+
+        try (ClientSession session = future.getSession()) {
+            IoSession ioSession = session.getIoSession();
+            SocketAddress remoteAddress = ioSession.getRemoteAddress();
+            String remoteLocation = toString(remoteAddress);
+            if (isEnabled(Level.FINE)) {
+                log(Level.FINE, "Connected to " + remoteLocation + " to retrieve key type=" + kt);
+            }
+
+            try {
+                session.addSessionListener(this);
+                if (isEnabled(Level.FINER)) {
+                    log(Level.FINER, "Authenticating with key type=" + kt + " to " + remoteLocation);
+                }
+
+                GenericUtils.forEach(ids, session::addPublicKeyIdentity);
+
+                try {
+                    // shouldn't really succeed, but do it since key exchange occurs only on auth attempt
+                    session.auth().verify(waitTime);
+                    log(Level.WARNING, "Unexpected authentication success using key type=" + kt + " with " + remoteLocation);
+                } catch (Exception e) {
+                    if (isEnabled(Level.FINER)) {
+                        log(Level.FINER, "Failed to authenticate using key type=" + kt + " with " + remoteLocation);
+                    }
+                } finally {
+                    GenericUtils.forEach(ids, session::removePublicKeyIdentity);
+                }
+            } finally {
+                session.removeSessionListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void sessionCreated(Session session) {
+        logSessionEvent(session, "Created");
+    }
+
+    @Override
+    public void sessionEvent(Session session, Event event) {
+        logSessionEvent(session, event);
+        if (isEnabled(Level.FINEST) && Event.KexCompleted.equals(event)) {
+            IoSession ioSession = session.getIoSession();
+            SocketAddress remoteAddress = ioSession.getRemoteAddress();
+            String remoteLocation = toString(remoteAddress);
+            for (KexProposalOption paramType : KexProposalOption.VALUES) {
+                String paramValue = session.getNegotiatedKexParameter(paramType);
+                log(Level.FINEST, remoteLocation + "[" + paramType.getDescription() + "]: " + paramValue);
+            }
+        }
+    }
+
+    @Override
+    public void sessionException(Session session, Throwable t) {
+        logSessionEvent(session, t);
+    }
+
+    @Override
+    public void sessionClosed(Session session) {
+        logSessionEvent(session, "Closed");
+    }
+
+    @Override
+    public void sessionNegotiationStart(
+            Session session, Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal) {
+        logSessionEvent(session, "sessionNegotiationStart");
+        logNegotiationProposal("c2s", clientProposal);
+        logNegotiationProposal("s2c", serverProposal);
+    }
+
+    protected void logNegotiationProposal(String type, Map<KexProposalOption, String> proposal) {
+        if (!isEnabled(Level.FINEST)) {
+            return;
+        }
+
+        proposal.forEach((option, value) -> log(Level.FINEST, option.getDescription() + "[" + type + "]: " + value));
+    }
+
+    @Override
+    public void sessionNegotiationEnd(Session session, Map<KexProposalOption, String> clientProposal,
+            Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
+            Throwable reason) {
+        if (reason == null) {
+            logSessionEvent(session, "sessionNegotiationStart");
+        } else {
+            logSessionEvent(session, reason);
+        }
+    }
+
+    protected void logSessionEvent(Session session, Object event) {
+        if (isEnabled(Level.FINEST)) {
+            IoSession ioSession = session.getIoSession();
+            SocketAddress remoteAddress = ioSession.getRemoteAddress();
+            log(Level.FINEST, "Session " + toString(remoteAddress) + " event: " + event);
+        }
+    }
+
+    @Override
+    public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+        String remoteLocation = toString(remoteAddress);
+        String extra = KeyUtils.getFingerPrint(serverKey);
+        try {
+            String keyType = KeyUtils.getKeyType(serverKey);
+            String current = GenericUtils.isEmpty(keyType) ? null : currentHostFingerprints.get(keyType);
+            if (Objects.equals(current, extra)) {
+                if (isEnabled(Level.FINER)) {
+                    log(Level.FINER, "verifyServerKey(" + remoteLocation + ")[" + keyType + "] skip existing key: " + extra);
+                }
+            } else {
+                if (isEnabled(Level.FINE)) {
+                    log(Level.FINE, "verifyServerKey(" + remoteLocation + ")[" + keyType + "] found new key: " + extra);
+                }
+
+                writeServerKey(remoteLocation, keyType, serverKey);
+
+                if (!GenericUtils.isEmpty(keyType)) {
+                    currentHostFingerprints.put(keyType, extra);
+                }
+            }
+        } catch (Exception e) {
+            log(Level.SEVERE, "Failed to output the public key " + extra + " from " + remoteLocation, e);
+        }
+
+        return true;
+    }
+
+    protected void writeServerKey(String remoteLocation, String keyType, PublicKey serverKey) throws Exception {
+        StringBuilder sb = new StringBuilder(256).append(remoteLocation).append(' ');
+        PublicKeyEntry.appendPublicKeyEntry(sb, serverKey);
+        log(Level.INFO, sb);
+    }
+
+    private static String toString(SocketAddress addr) {
+        if (addr == null) {
+            return null;
+        } else if (addr instanceof InetSocketAddress) {
+            return ((InetSocketAddress) addr).getHostString();
+        } else if (addr instanceof SshdSocketAddress) {
+            return ((SshdSocketAddress) addr).getHostName();
+        } else {
+            return addr.toString();
+        }
+    }
+
+    protected List<NamedFactory<Signature>> resolveSignatureFactories(String keyType) throws GeneralSecurityException {
+        if (isEnabled(Level.FINE)) {
+            log(Level.FINE, "Resolve signature factories for " + keyType);
+        }
+
+        if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
+            return Collections.singletonList(BuiltinSignatures.rsa);
+        } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
+            return Collections.singletonList(BuiltinSignatures.dsa);
+        } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
+            List<NamedFactory<Signature>> factories = new ArrayList<>(ECCurves.NAMES.size());
+            for (String n : ECCurves.NAMES) {
+                if (isEnabled(Level.FINER)) {
+                    log(Level.FINER, "Resolve signature factory for curve=" + n);
+                }
+
+                NamedFactory<Signature> f =
+                        ValidateUtils.checkNotNull(BuiltinSignatures.fromString(n), "Unknown curve signature: %s", n);
+                factories.add(f);
+            }
+
+            return factories;
+        } else if (BuiltinIdentities.Constants.ED25519.equalsIgnoreCase(keyType)) {
+            return Collections.singletonList(BuiltinSignatures.ed25519);
+        } else {
+            throw new NoSuchAlgorithmException("Unknown key type: " + keyType);
+        }
+    }
+
+    protected Map<String, List<KeyPair>> createKeyPairs(Collection<String> typeNames) throws GeneralSecurityException {
+        if (GenericUtils.isEmpty(typeNames)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, List<KeyPair>> pairsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (String kt : typeNames) {
+            if ("*".equalsIgnoreCase(kt) || "all".equalsIgnoreCase(kt)) {
+                ValidateUtils.checkTrue(typeNames.size() == 1, "Wildcard key type must be the only one specified: %s", typeNames);
+                return createKeyPairs(BuiltinIdentities.NAMES);
+            }
+
+            if (pairsMap.containsKey(kt)) {
+                log(Level.WARNING, "Key type " + kt + " re-specified");
+                continue;
+            }
+
+            List<KeyPair> kps = createKeyPairs(kt);
+            if (GenericUtils.isEmpty(kps)) {
+                log(Level.WARNING, "No key-pairs generated for key type " + kt);
+                continue;
+            }
+
+            pairsMap.put(kt, kps);
+        }
+
+        return pairsMap;
+    }
+
+    protected List<KeyPair> createKeyPairs(String keyType) throws GeneralSecurityException {
+        if (isEnabled(Level.FINE)) {
+            log(Level.FINE, "Generate key pairs for " + keyType);
+        }
+
+        if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
+            return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_RSA, 1024));
+        } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
+            return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_DSS, 512));
+        } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
+            if (!SecurityUtils.isECCSupported()) {
+                throw new NoSuchAlgorithmException("ECC not supported: " + keyType);
+            }
+
+            List<KeyPair> kps = new ArrayList<>(ECCurves.NAMES.size());
+            for (ECCurves curve : ECCurves.VALUES) {
+                String curveName = curve.getName();
+                if (isEnabled(Level.FINER)) {
+                    log(Level.FINER, "Generate key pair for curve=" + curveName);
+                }
+
+                kps.add(KeyUtils.generateKeyPair(curve.getKeyType(), curve.getKeySize()));
+            }
+
+            return kps;
+        } else if (BuiltinIdentities.Constants.ED25519.equalsIgnoreCase(keyType)) {
+            if (!SecurityUtils.isEDDSACurveSupported()) {
+                throw new NoSuchAlgorithmException("EDDSA curves not supported: " + keyType);
+            }
+
+            KeyPairGenerator g = SecurityUtils.getKeyPairGenerator(SecurityUtils.EDDSA);
+            return Collections.singletonList(g.generateKeyPair());
+        } else {
+            throw new InvalidKeySpecException("Unknown key type: " + keyType);
+        }
+    }
+
+    @Override
+    public boolean isOpen() {
+        return open.get();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (!open.getAndSet(false)) {
+            return; // already closed
+        }
+
+        IOException err = null;
+        if (input != null) {
+            try {
+                input.close();
+            } catch (IOException e) {
+                err = GenericUtils.accumulateException(err, e);
+            } finally {
+                input = null;
+            }
+        }
+
+        if (client != null) {
+            try {
+                client.close();
+            } catch (IOException e) {
+                err = GenericUtils.accumulateException(err, e);
+            } finally {
+                try {
+                    client.stop();
+                } finally {
+                    client = null;
+                }
+            }
+        }
+        if (err != null) {
+            throw err;
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    // returns a List of the hosts to be contacted
+    public static List<String> parseCommandLineArguments(SshKeyScanMain scanner, String... args) throws IOException {
+        int numArgs = GenericUtils.length(args);
+        for (int index = 0; index < numArgs; index++) {
+            String optName = args[index];
+            if ("-f".equals(optName)) {
+                index++;
+                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
+                ValidateUtils.checkTrue(scanner.getInputStream() == null, "%s option re-specified", optName);
+
+                String filePath = args[index];
+                if ("-".equals(filePath)) {
+                    scanner.setInputStream(new NoCloseInputStream(System.in));
+                } else {
+                    scanner.setInputStream(new FileInputStream(filePath));
+                }
+            } else if ("-t".equals(optName)) {
+                index++;
+                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
+                ValidateUtils.checkTrue(GenericUtils.isEmpty(scanner.getKeyTypes()), "%s option re-specified", optName);
+
+                String typeList = args[index];
+                String[] types = GenericUtils.split(typeList, ',');
+                ValidateUtils.checkTrue(GenericUtils.length(types) > 0, "No types specified for %s", optName);
+                scanner.setKeyTypes(Arrays.asList(types));
+            } else if ("-p".equals(optName)) {
+                index++;
+                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
+                ValidateUtils.checkTrue(scanner.getPort() <= 0, "%s option re-specified", optName);
+
+                String portValue = args[index];
+                int port = Integer.parseInt(portValue);
+                ValidateUtils.checkTrue((port > 0) && (port <= 0xFFFF), "Bad port: %s", portValue);
+                scanner.setPort(port);
+            } else if ("-T".equals(optName)) {
+                index++;
+                ValidateUtils.checkTrue(index < numArgs, "Missing %s option argument", optName);
+                ValidateUtils.checkTrue(scanner.getTimeout() <= 0, "%s option re-specified", optName);
+
+                String timeoutValue = args[index];
+                long timeout = Long.parseLong(timeoutValue);
+                ValidateUtils.checkTrue(timeout > 0L, "Bad timeout: %s", timeoutValue);
+                scanner.setTimeout(timeout);
+            } else if ("-v".equals(optName)) {
+                ValidateUtils.checkTrue(scanner.getLogLevel() == null, "%s option re-specified", optName);
+                scanner.setLogLevel(Level.FINEST);
+            } else {    // stop at first non-option - assume the rest are host names/addresses
+                ValidateUtils.checkTrue(optName.charAt(0) != '-', "Unknown option: %s", optName);
+
+                int remaining = numArgs - index;
+                if (remaining == 1) {
+                    return Collections.singletonList(optName);
+                }
+
+                List<String> hosts = new ArrayList<>(remaining);
+                for (; index < numArgs; index++) {
+                    hosts.add(args[index]);
+                }
+
+                return hosts;
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
+    /* -------------------------------------------------------------------- */
+
+    public static <S extends SshKeyScanMain> S setInputStream(S scanner, Collection<String> hosts) throws IOException {
+        if (GenericUtils.isEmpty(hosts)) {
+            Objects.requireNonNull(scanner.getInputStream(), "No hosts or file specified");
+        } else {
+            ValidateUtils.checkTrue(scanner.getInputStream() == null, "Both hosts and file specified");
+
+            // convert the hosts from the arguments into a "file" - one host per line
+            try (ByteArrayOutputStream baos = new ByteArrayOutputStream(hosts.size() * 32)) {
+                try (Writer w = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) {
+                    for (String h : hosts) {
+                        w.append(h).append(IoUtils.EOL);
+                    }
+                }
+
+                byte[] data = baos.toByteArray();
+                scanner.setInputStream(new ByteArrayInputStream(data));
+            }
+        }
+
+        return scanner;
+    }
+
+    public static <S extends SshKeyScanMain> S initializeScanner(S scanner, Collection<String> hosts) throws IOException {
+        setInputStream(scanner, hosts);
+        if (scanner.getPort() <= 0) {
+            scanner.setPort(SshConfigFileReader.DEFAULT_PORT);
+        }
+
+        if (scanner.getTimeout() <= 0L) {
+            scanner.setTimeout(DEFAULT_TIMEOUT);
+        }
+
+        if (GenericUtils.isEmpty(scanner.getKeyTypes())) {
+            scanner.setKeyTypes(DEFAULT_KEY_TYPES);
+        }
+
+        if (scanner.getLogLevel() == null) {
+            scanner.setLogLevel(DEFAULT_LEVEL);
+        }
+
+        return scanner;
+    }
+
+    /* -------------------------------------------------------------------- */
+
+    public static void main(String[] args) throws Exception {
+        try (SshKeyScanMain scanner = new SshKeyScanMain()) {
+            Collection<String> hosts = parseCommandLineArguments(scanner, args);
+            initializeScanner(scanner, hosts);
+            scanner.call();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
new file mode 100644
index 0000000..c0ccc32
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.util.test.BaseTestSupport;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ChannelExecMain extends BaseTestSupport {
+    public static void doExecuteCommands(
+            BufferedReader stdin, PrintStream stdout, PrintStream stderr, ClientSession session) throws Exception {
+        while (true) {
+            stdout.print("> ");
+
+            String command = stdin.readLine();
+            if ("q".equalsIgnoreCase(command) || "quit".equalsIgnoreCase(command)) {
+                break;
+            }
+            if (GenericUtils.isEmpty(command)) {
+                continue;
+            }
+
+            while (true) {
+                try {
+                    String response = session.executeRemoteCommand(command);
+                    String[] lines = GenericUtils.split(response, '\n');
+                    for (String l : lines) {
+                        stdout.append('\t').println(l);
+                    }
+                } catch (Exception e) {
+                    stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
+                }
+
+                stdout.append("Execute ").append(command).print(" again [y]/n ");
+                String ans = stdin.readLine();
+                if ((GenericUtils.length(ans) > 0) && (Character.toLowerCase(ans.charAt(0)) != 'y')) {
+                    break;
+                }
+            }
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        PrintStream stdout = System.out;
+        PrintStream stderr = System.err;
+        try (BufferedReader stdin = new BufferedReader(
+                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
+            ClientSession session = SshClientCliSupport.setupClientSession("-P", stdin, stdout, stderr, args);
+            if (session == null) {
+                System.err.println("usage: channelExec [-i identity] [-l login] [-P port] [-o option=value]"
+                        + " [-w password] [-c cipherlist]  [-m maclist] [-C] hostname/user@host");
+                System.exit(-1);
+                return;
+            }
+
+            try (SshClient client = (SshClient) session.getFactoryManager()) {
+                try (ClientSession clientSession = session) {
+                    doExecuteCommands(stdin, stdout, stderr, session);
+                } finally {
+                    client.stop();
+                }
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/test/java/org/apache/sshd/cli/client/ScpCommandMainDevelopment.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ScpCommandMainDevelopment.java b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ScpCommandMainDevelopment.java
new file mode 100644
index 0000000..833dca2
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ScpCommandMainDevelopment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+/**
+ * Just a test class used to invoke {@link ScpCommandMain#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class ScpCommandMainDevelopment {
+    private ScpCommandMainDevelopment() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static void main(String[] args) throws Exception {
+        ScpCommandMain.main(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/test/java/org/apache/sshd/cli/client/SftpCommandMainDevelopment.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/client/SftpCommandMainDevelopment.java b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SftpCommandMainDevelopment.java
new file mode 100644
index 0000000..07fb170
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SftpCommandMainDevelopment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+/**
+ * Just a test class used to invoke {@link SftpCommandMain#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SftpCommandMainDevelopment {
+    private SftpCommandMainDevelopment() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static void main(String[] args) throws Exception {
+        SftpCommandMain.main(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshClientMainDevelopment.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshClientMainDevelopment.java b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshClientMainDevelopment.java
new file mode 100644
index 0000000..e36776a
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshClientMainDevelopment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+/**
+ * Just a test class used to invoke {@link SshClientMain#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SshClientMainDevelopment {
+    private SshClientMainDevelopment() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static void main(String[] args) throws Exception {
+        SshClientMain.main(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshKeyScanMainDevelopment.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshKeyScanMainDevelopment.java b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshKeyScanMainDevelopment.java
new file mode 100644
index 0000000..9d38925
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/SshKeyScanMainDevelopment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.client;
+
+/**
+ * Just a test class used to invoke {@link SshKeyScanMain#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SshKeyScanMainDevelopment {
+    private SshKeyScanMainDevelopment() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static void main(String[] args) throws Exception {
+        SshKeyScanMain.main(args);
+    }
+}


[3/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for client commands to sshd-cli module

Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
deleted file mode 100644
index 61a83ec..0000000
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
+++ /dev/null
@@ -1,920 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client.subsystem.sftp;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.nio.channels.Channel;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.logging.Level;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
-import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
-import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
-import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.kex.KexProposalOption;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.subsystem.sftp.SftpException;
-import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
-import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.buffer.BufferUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-
-/**
- * Implements a simple command line SFTP client similar to the Linux one
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class SftpCommand implements Channel {
-    /**
-     * Command line option used to indicate a non-default port number
-     */
-    public static final String SFTP_PORT_OPTION = "-P";
-
-    private final SftpClient client;
-    private final Map<String, CommandExecutor> commandsMap;
-    private String cwdRemote;
-    private String cwdLocal;
-
-    @SuppressWarnings("synthetic-access")
-    public SftpCommand(SftpClient client) {
-        this.client = Objects.requireNonNull(client, "No client");
-
-        Map<String, CommandExecutor> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        for (CommandExecutor e : Arrays.asList(
-                new ExitCommandExecutor(),
-                new PwdCommandExecutor(),
-                new InfoCommandExecutor(),
-                new SessionCommandExecutor(),
-                new VersionCommandExecutor(),
-                new CdCommandExecutor(),
-                new LcdCommandExecutor(),
-                new MkdirCommandExecutor(),
-                new LsCommandExecutor(),
-                new LStatCommandExecutor(),
-                new ReadLinkCommandExecutor(),
-                new RmCommandExecutor(),
-                new RmdirCommandExecutor(),
-                new RenameCommandExecutor(),
-                new StatVfsCommandExecutor(),
-                new GetCommandExecutor(),
-                new PutCommandExecutor(),
-                new HelpCommandExecutor()
-        )) {
-            String name = e.getName();
-            ValidateUtils.checkTrue(map.put(name, e) == null, "Multiple commands named '%s'", name);
-        }
-        commandsMap = Collections.unmodifiableMap(map);
-        cwdLocal = System.getProperty("user.dir");
-    }
-
-    public final SftpClient getClient() {
-        return client;
-    }
-
-    public void doInteractive(BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-        SftpClient sftp = getClient();
-        setCurrentRemoteDirectory(sftp.canonicalPath("."));
-        while (true) {
-            stdout.append(getCurrentRemoteDirectory()).append(" > ").flush();
-            String line = stdin.readLine();
-            if (line == null) { // EOF
-                break;
-            }
-
-            line = GenericUtils.replaceWhitespaceAndTrim(line);
-            if (GenericUtils.isEmpty(line)) {
-                continue;
-            }
-
-            String cmd;
-            String args;
-            int pos = line.indexOf(' ');
-            if (pos > 0) {
-                cmd = line.substring(0, pos);
-                args = line.substring(pos + 1).trim();
-            } else {
-                cmd = line;
-                args = "";
-            }
-
-            CommandExecutor exec = commandsMap.get(cmd);
-            try {
-                if (exec == null) {
-                    stderr.append("Unknown command: ").println(line);
-                } else {
-                    try {
-                        if (exec.executeCommand(args, stdin, stdout, stderr)) {
-                            break;
-                        }
-                    } catch (Exception e) {
-                        stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
-                    } finally {
-                        stdout.flush();
-                    }
-                }
-            } finally {
-                stderr.flush(); // just makings sure
-            }
-        }
-    }
-
-    protected String resolveLocalPath(String pathArg) {
-        String cwd = getCurrentLocalDirectory();
-        if (GenericUtils.isEmpty(pathArg)) {
-            return cwd;
-        }
-
-        if (OsUtils.isWin32()) {
-            if ((pathArg.length() >= 2) && (pathArg.charAt(1) == ':')) {
-                return pathArg;
-            }
-        } else {
-            if (pathArg.charAt(0) == '/') {
-                return pathArg;
-            }
-        }
-
-        return cwd + File.separator + pathArg.replace('/', File.separatorChar);
-    }
-
-    protected String resolveRemotePath(String pathArg) {
-        String cwd = getCurrentRemoteDirectory();
-        if (GenericUtils.isEmpty(pathArg)) {
-            return cwd;
-        }
-
-        if (pathArg.charAt(0) == '/') {
-            return pathArg;
-        } else {
-            return cwd + "/" + pathArg;
-        }
-    }
-
-    protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException {
-        stdout.append('\t').append(Long.toString(attrs.getSize()))
-              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
-        if (attrs.isSymbolicLink()) {
-            String linkValue = sftp.readLink(path);
-            stdout.append(" => ")
-                  .append('(').append(attrs.isDirectory() ? "dir" : "file").append(')')
-                  .append(' ').append(linkValue);
-        }
-
-        return stdout;
-    }
-
-    public String getCurrentRemoteDirectory() {
-        return cwdRemote;
-    }
-
-    public void setCurrentRemoteDirectory(String path) {
-        cwdRemote = path;
-    }
-
-    public String getCurrentLocalDirectory() {
-        return cwdLocal;
-    }
-
-    public void setCurrentLocalDirectory(String path) {
-        cwdLocal = path;
-    }
-
-    @Override
-    public boolean isOpen() {
-        return client.isOpen();
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (isOpen()) {
-            client.close();
-        }
-    }
-
-    public interface CommandExecutor extends NamedResource {
-        // return value is whether to stop running
-        boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception;
-    }
-
-    //////////////////////////////////////////////////////////////////////////
-
-    public static <A extends Appendable> A appendInfoValue(A sb, CharSequence name, Object value) throws IOException {
-        sb.append('\t').append(name).append(": ").append(Objects.toString(value));
-        return sb;
-    }
-
-    public static void main(String[] args) throws Exception {
-        PrintStream stdout = System.out;
-        PrintStream stderr = System.err;
-        OutputStream logStream = stderr;
-        try (BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
-            Level level = SshClient.resolveLoggingVerbosity(args);
-            logStream = SshClient.resolveLoggingTargetStream(stdout, stderr, args);
-            if (logStream != null) {
-                SshClient.setupLogging(level, stdout, stderr, logStream);
-            }
-
-            ClientSession session = (logStream == null) ? null : SshClient.setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args);
-            if (session == null) {
-                System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i identity]"
-                        + " [-l login] [" + SFTP_PORT_OPTION + " port] [-o option=value]"
-                        + " [-w password] [-c cipherlist]  [-m maclist] [-C] hostname/user@host");
-                System.exit(-1);
-                return;
-            }
-
-            try {
-                try (SftpCommand sftp = new SftpCommand(SftpClientFactory.instance().createSftpClient(session))) {
-                    sftp.doInteractive(stdin, stdout, stderr);
-                }
-            } finally {
-                session.close();
-            }
-        } finally {
-            if ((logStream != stdout) && (logStream != stderr)) {
-                logStream.close();
-            }
-        }
-    }
-
-    private static class ExitCommandExecutor implements CommandExecutor {
-        ExitCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "exit";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            stdout.println("Exiting");
-            return true;
-        }
-    }
-
-    private class PwdCommandExecutor implements CommandExecutor {
-        protected PwdCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "pwd";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            stdout.append('\t').append("Remote: ").println(getCurrentRemoteDirectory());
-            stdout.append('\t').append("Local: ").println(getCurrentLocalDirectory());
-            return false;
-        }
-    }
-
-    private class SessionCommandExecutor implements CommandExecutor {
-        SessionCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "session";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            SftpClient sftp = getClient();
-            ClientSession session = sftp.getSession();
-            appendInfoValue(stdout, "Session ID", BufferUtils.toHex(session.getSessionId())).println();
-            appendInfoValue(stdout, "Connect address", session.getConnectAddress()).println();
-
-            IoSession ioSession = session.getIoSession();
-            appendInfoValue(stdout, "Local address", ioSession.getLocalAddress()).println();
-            appendInfoValue(stdout, "Remote address", ioSession.getRemoteAddress()).println();
-
-            for (KexProposalOption option : KexProposalOption.VALUES) {
-                appendInfoValue(stdout, option.getDescription(), session.getNegotiatedKexParameter(option)).println();
-            }
-
-            return false;
-        }
-    }
-
-    private class InfoCommandExecutor implements CommandExecutor {
-        InfoCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "info";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            SftpClient sftp = getClient();
-            Session session = sftp.getSession();
-            stdout.append('\t').println(session.getServerVersion());
-
-            Map<String, byte[]> extensions = sftp.getServerExtensions();
-            Map<String, ?> parsed = ParserUtils.parse(extensions);
-            if (GenericUtils.size(extensions) > 0) {
-                stdout.println();
-            }
-
-            extensions.forEach((name, value) -> {
-                Object info = parsed.get(name);
-
-                stdout.append('\t').append(name).append(": ");
-                if (info == null) {
-                    stdout.println(BufferUtils.toHex(value));
-                } else {
-                    stdout.println(info);
-                }
-            });
-
-            return false;
-        }
-    }
-
-    private class VersionCommandExecutor implements CommandExecutor {
-        VersionCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "version";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            SftpClient sftp = getClient();
-            stdout.append('\t').println(sftp.getVersion());
-            return false;
-        }
-    }
-
-    private class CdCommandExecutor extends PwdCommandExecutor {
-        CdCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "cd";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
-
-            String newPath = resolveRemotePath(args);
-            SftpClient sftp = getClient();
-            setCurrentRemoteDirectory(sftp.canonicalPath(newPath));
-            return super.executeCommand("", stdin, stdout, stderr);
-        }
-    }
-
-    private class LcdCommandExecutor extends PwdCommandExecutor {
-        LcdCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "lcd";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            if (GenericUtils.isEmpty(args)) {
-                setCurrentLocalDirectory(System.getProperty("user.home"));
-            } else {
-                Path path = Paths.get(resolveLocalPath(args)).normalize().toAbsolutePath();
-                ValidateUtils.checkTrue(Files.exists(path), "No such local directory: %s", path);
-                ValidateUtils.checkTrue(Files.isDirectory(path), "Path is not a directory: %s", path);
-                setCurrentLocalDirectory(path.toString());
-            }
-
-            return super.executeCommand("", stdin, stdout, stderr);
-        }
-    }
-
-    private class MkdirCommandExecutor implements CommandExecutor {
-        MkdirCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "mkdir";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
-
-            String path = resolveRemotePath(args);
-            SftpClient sftp = getClient();
-            sftp.mkdir(path);
-            return false;
-        }
-    }
-
-    private class LsCommandExecutor implements CommandExecutor {
-        LsCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "ls";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            int numComps = GenericUtils.length(comps);
-            String pathArg = (numComps <= 0) ? null : GenericUtils.trimToEmpty(comps[numComps - 1]);
-            String flags = (numComps >= 2) ? GenericUtils.trimToEmpty(comps[0]) : null;
-            // ignore all flags
-            if ((GenericUtils.length(pathArg) > 0) && (pathArg.charAt(0) == '-')) {
-                flags = pathArg;
-                pathArg = null;
-            }
-
-            String path = resolveRemotePath(pathArg);
-            SftpClient sftp = getClient();
-            int version = sftp.getVersion();
-            boolean showLongName = (version == SftpConstants.SFTP_V3) && (GenericUtils.length(flags) > 1) && (flags.indexOf('l') > 0);
-            for (SftpClient.DirEntry entry : sftp.readDir(path)) {
-                String fileName = entry.getFilename();
-                SftpClient.Attributes attrs = entry.getAttributes();
-                appendFileAttributes(stdout.append('\t').append(fileName), sftp, path + "/" + fileName, attrs).println();
-                if (showLongName) {
-                    stdout.append("\t\tlong-name: ").println(entry.getLongFilename());
-                }
-            }
-
-            return false;
-        }
-    }
-
-    private class RmCommandExecutor implements CommandExecutor {
-        RmCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "rm";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            int numArgs = GenericUtils.length(comps);
-            ValidateUtils.checkTrue(numArgs >= 1, "No arguments");
-            ValidateUtils.checkTrue(numArgs <= 2, "Too many arguments: %s", args);
-
-            String remotePath = comps[0];
-            boolean recursive = false;
-            boolean verbose = false;
-            if (remotePath.charAt(0) == '-') {
-                ValidateUtils.checkTrue(remotePath.length() > 1, "Missing flags specification: %s", args);
-                ValidateUtils.checkTrue(numArgs == 2, "Missing remote directory: %s", args);
-
-                for (int index = 1; index < remotePath.length(); index++) {
-                    char ch = remotePath.charAt(index);
-                    switch(ch) {
-                        case 'r' :
-                            recursive = true;
-                            break;
-                        case 'v':
-                            verbose = true;
-                            break;
-                        default:
-                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
-                    }
-                }
-                remotePath = comps[1];
-            }
-
-            String path = resolveRemotePath(remotePath);
-            SftpClient sftp = getClient();
-            if (recursive) {
-                Attributes attrs = sftp.stat(path);
-                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", args);
-                removeRecursive(sftp, path, attrs, stdout, verbose);
-            } else {
-                sftp.remove(path);
-                if (verbose) {
-                    stdout.append('\t').append("Removed ").println(path);
-                }
-            }
-
-            return false;
-        }
-
-        private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
-            if (attrs.isDirectory()) {
-                for (DirEntry entry : sftp.readDir(path)) {
-                    String name = entry.getFilename();
-                    if (".".equals(name) || "..".equals(name)) {
-                        continue;
-                    }
-
-                    removeRecursive(sftp, path + "/" + name, entry.getAttributes(), stdout, verbose);
-                }
-
-                sftp.rmdir(path);
-            } else if (attrs.isRegularFile()) {
-                sftp.remove(path);
-            } else {
-                if (verbose) {
-                    stdout.append('\t').append("Skip special file ").println(path);
-                    return;
-                }
-            }
-
-            if (verbose) {
-                stdout.append('\t').append("Removed ").println(path);
-            }
-        }
-    }
-
-    private class RmdirCommandExecutor implements CommandExecutor {
-        RmdirCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "rmdir";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
-
-            String path = resolveRemotePath(args);
-            SftpClient sftp = getClient();
-            sftp.rmdir(path);
-            return false;
-        }
-    }
-
-    private class RenameCommandExecutor implements CommandExecutor {
-        RenameCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "rename";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args);
-
-            String oldPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[0]));
-            String newPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[1]));
-            SftpClient sftp = getClient();
-            sftp.rename(oldPath, newPath);
-            return false;
-        }
-    }
-
-    private class StatVfsCommandExecutor implements CommandExecutor {
-        StatVfsCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return StatVfsExtensionParser.NAME;
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            int numArgs = GenericUtils.length(comps);
-            ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args);
-
-            SftpClient sftp = getClient();
-            OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
-            ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
-
-            String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
-            OpenSSHStatExtensionInfo info = ext.stat(remPath);
-            Field[] fields = info.getClass().getFields();
-            for (Field f : fields) {
-                String name = f.getName();
-                int mod = f.getModifiers();
-                if (Modifier.isStatic(mod)) {
-                    continue;
-                }
-
-                Object value = f.get(info);
-                stdout.append('\t').append(name).append(": ").println(value);
-            }
-
-            return false;
-        }
-    }
-
-    private class LStatCommandExecutor implements CommandExecutor {
-        LStatCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "lstat";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
-
-            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
-            SftpClient client = getClient();
-            Attributes attrs = client.lstat(path);
-            appendFileAttributes(stdout, client, path, attrs).println();
-            return false;
-        }
-    }
-
-    private class ReadLinkCommandExecutor implements CommandExecutor {
-        ReadLinkCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "readlink";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            String[] comps = GenericUtils.split(args, ' ');
-            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
-
-            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
-            SftpClient client = getClient();
-            String linkData = client.readLink(path);
-            stdout.append('\t').println(linkData);
-            return false;
-        }
-    }
-
-    private class HelpCommandExecutor implements CommandExecutor {
-        HelpCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "help";
-        }
-
-        @Override
-        @SuppressWarnings("synthetic-access")
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
-            for (String cmd : commandsMap.keySet()) {
-                stdout.append('\t').println(cmd);
-            }
-            return false;
-        }
-    }
-
-    private abstract class TransferCommandExecutor implements CommandExecutor {
-        protected TransferCommandExecutor() {
-            super();
-        }
-
-        protected void createDirectories(SftpClient sftp, String remotePath) throws IOException {
-            try {
-                Attributes attrs = sftp.stat(remotePath);
-                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path already exists but is not a directory: %s", remotePath);
-                return;
-            } catch (SftpException e) {
-                int status = e.getStatus();
-                ValidateUtils.checkTrue(status == SftpConstants.SSH_FX_NO_SUCH_FILE, "Failed to get status of %s: %s", remotePath, e.getMessage());
-            }
-
-            int pos = remotePath.lastIndexOf('/');
-            ValidateUtils.checkTrue(pos > 0, "No more parents for %s", remotePath);
-            createDirectories(sftp, remotePath.substring(0, pos));
-        }
-
-        protected void transferFile(SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose) throws IOException {
-            // Create the file's hierarchy
-            if (upload) {
-                int pos = remotePath.lastIndexOf('/');
-                ValidateUtils.checkTrue(pos > 0, "Missing full remote file path: %s", remotePath);
-                createDirectories(sftp, remotePath.substring(0, pos));
-            } else {
-                Files.createDirectories(localPath.getParent());
-            }
-
-            try (InputStream input = upload ? Files.newInputStream(localPath) : sftp.read(remotePath);
-                 OutputStream output = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath)) {
-                IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE);
-            }
-
-            if (verbose) {
-                stdout.append('\t')
-                      .append("Copied ").append(upload ? localPath.toString() : remotePath)
-                      .append(" to ").println(upload ? remotePath : localPath.toString());
-            }
-        }
-
-        protected void transferRemoteDir(SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
-            if (attrs.isDirectory()) {
-                for (DirEntry entry : sftp.readDir(remotePath)) {
-                    String name = entry.getFilename();
-                    if (".".equals(name) || "..".equals(name)) {
-                        continue;
-                    }
-
-                    transferRemoteDir(sftp, localPath.resolve(name), remotePath + "/" + name, entry.getAttributes(), stdout, verbose);
-                }
-            } else if (attrs.isRegularFile()) {
-                transferFile(sftp, localPath, remotePath, false, stdout, verbose);
-            } else {
-                if (verbose) {
-                    stdout.append('\t').append("Skip remote special file ").println(remotePath);
-                }
-            }
-        }
-
-        protected void transferLocalDir(SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose) throws IOException {
-            if (Files.isDirectory(localPath)) {
-                try (DirectoryStream<Path> ds = Files.newDirectoryStream(localPath)) {
-                    for (Path entry : ds) {
-                        String name = entry.getFileName().toString();
-                        transferLocalDir(sftp, localPath.resolve(name), remotePath + "/" + name, stdout, verbose);
-                    }
-                }
-            } else if (Files.isRegularFile(localPath)) {
-                transferFile(sftp, localPath, remotePath, true, stdout, verbose);
-            } else {
-                if (verbose) {
-                    stdout.append('\t').append("Skip local special file ").println(localPath);
-                }
-            }
-        }
-
-        protected void executeCommand(String args, boolean upload, PrintStream stdout) throws IOException {
-            String[] comps = GenericUtils.split(args, ' ');
-            int numArgs = GenericUtils.length(comps);
-            ValidateUtils.checkTrue((numArgs >= 1) && (numArgs <= 3), "Invalid number of arguments: %s", args);
-
-            String src = comps[0];
-            boolean recursive = false;
-            boolean verbose = false;
-            int tgtIndex = 1;
-            if (src.charAt(0) == '-') {
-                ValidateUtils.checkTrue(src.length() > 1, "Missing flags specification: %s", args);
-                ValidateUtils.checkTrue(numArgs >= 2, "Missing source specification: %s", args);
-
-                for (int index = 1; index < src.length(); index++) {
-                    char ch = src.charAt(index);
-                    switch(ch) {
-                        case 'r' :
-                            recursive = true;
-                            break;
-                        case 'v':
-                            verbose = true;
-                            break;
-                        default:
-                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
-                    }
-                }
-                src = comps[1];
-                tgtIndex++;
-            }
-
-            String tgt = (tgtIndex < numArgs) ? comps[tgtIndex] : null;
-            String localPath;
-            String remotePath;
-            if (upload) {
-                localPath = src;
-                remotePath = ValidateUtils.checkNotNullAndNotEmpty(tgt, "No remote target specified: %s", args);
-            } else {
-                localPath = GenericUtils.isEmpty(tgt) ? getCurrentLocalDirectory() : tgt;
-                remotePath = src;
-            }
-
-            SftpClient sftp = getClient();
-            Path local = Paths.get(resolveLocalPath(localPath)).normalize().toAbsolutePath();
-            String remote = resolveRemotePath(remotePath);
-            if (recursive) {
-                if (upload) {
-                    ValidateUtils.checkTrue(Files.isDirectory(local), "Local path not a directory or does not exist: %s", local);
-                    transferLocalDir(sftp, local, remote, stdout, verbose);
-                } else {
-                    Attributes attrs = sftp.stat(remote);
-                    ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", remote);
-                    transferRemoteDir(sftp, local, remote, attrs, stdout, verbose);
-                }
-            } else {
-                if (Files.exists(local) && Files.isDirectory(local)) {
-                    int pos = remote.lastIndexOf('/');
-                    String name = (pos >= 0) ? remote.substring(pos + 1) : remote;
-                    local = local.resolve(name);
-                }
-
-                transferFile(sftp, local, remote, upload, stdout, verbose);
-            }
-        }
-    }
-
-    private class GetCommandExecutor extends TransferCommandExecutor {
-        GetCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "get";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            executeCommand(args, false, stdout);
-            return false;
-        }
-    }
-
-    private class PutCommandExecutor extends TransferCommandExecutor {
-        PutCommandExecutor() {
-            super();
-        }
-
-        @Override
-        public String getName() {
-            return "put";
-        }
-
-        @Override
-        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            executeCommand(args, true, stdout);
-            return false;
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java
deleted file mode 100644
index 4b34f92..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.client.subsystem.sftp;
-
-/**
- * Just a test class used to invoke {@link SftpCommand#main(String[])} in
- * order to have logging - which is in {@code test} scope
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class SftpCommandMain {
-    private SftpCommandMain() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    public static void main(String[] args) throws Exception {
-        SftpCommand.main(args);
-    }
-}


[2/6] mina-sshd git commit: [SSHD-816] Provide some way for the SSHD command line server to detect and configure the (split) SFTP subsystem

Posted by lg...@apache.org.
[SSHD-816] Provide some way for the SSHD command line server to detect and configure the (split) SFTP subsystem


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/2b7e22a3
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/2b7e22a3
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/2b7e22a3

Branch: refs/heads/master
Commit: 2b7e22a31fb7ae0c3cb1933304b3d6d57ad54212
Parents: 536effd
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Tue Apr 17 12:15:54 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Tue Apr 17 15:32:31 2018 +0300

----------------------------------------------------------------------
 README.md                                       |  16 +
 assembly/src/main/distribution/bin/sshd.bat     |   2 +-
 assembly/src/main/distribution/bin/sshd.sh      |   2 +-
 .../java/org/apache/sshd/cli/CliSupport.java    |   1 -
 .../sshd/cli/server/SshServerCliSupport.java    | 184 +++++++++++
 .../apache/sshd/cli/server/SshServerMain.java   | 210 ++++++++++++
 .../apache/sshd/cli/server/SshFsMounter.java    | 328 +++++++++++++++++++
 .../cli/server/SshServerMainDevelopment.java    |  36 ++
 .../java/org/apache/sshd/server/SshServer.java  | 240 --------------
 .../org/apache/sshd/server/SshServerMain.java   |  36 --
 ...pache.sshd.server.subsystem.SubsystemFactory |  20 ++
 .../server/subsystem/sftp/SshFsMounter.java     | 327 ------------------
 12 files changed, 796 insertions(+), 606 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index bcef46f..bb4f586 100644
--- a/README.md
+++ b/README.md
@@ -1293,6 +1293,22 @@ In order to use this CLI code as part of another project, one needs to include t
     </dependency>
 ```
 
+### Command line SSH daemon
+
+* **Port** - by default the SSH server sets up to list on port 8000 in order to avoid conflicts with any running SSH O/S daemon. This can be modified by providing a `-p NNNN`
+or `-o Port=NNNN` command line option.
+
+* **Subsystem(s)** - the server automatically detects subsystems using the [Java ServiceLoader mechanism](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
+This can be overwritten as follows (in this order):
+
+1. Provide a `org.apache.sshd.server.subsystem.SubsystemFactory` system property containing comma-separated fully-qualified names of classes implementing
+this interface. The implementations must have a public no-args constructor for instantiating them. The order of the provided subsystems will be according
+to their order in the specified list
+
+2. Provide a `-o Subsystem=xxx,yyy` command line argument where value is a comma-separated list of the **name**(s) of the auto-detected factories via
+the `ServiceLoader` mechanism. The special value `none` may be used to indicate that no subsystem is to be configured. **Note:** no specific order is
+provided when subsystems are auto-detected and/or filtered.
+
 ## GIT support
 
 The _sshd-git_ artifact contains both client and server-side command factories for issuing and handling some _git_ commands. The code is based on [JGit](https://github.com/eclipse/jgit)

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/assembly/src/main/distribution/bin/sshd.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sshd.bat b/assembly/src/main/distribution/bin/sshd.bat
index 0863f7f..86d740f 100644
--- a/assembly/src/main/distribution/bin/sshd.bat
+++ b/assembly/src/main/distribution/bin/sshd.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.server.SshServer %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.server.SshServerMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/assembly/src/main/distribution/bin/sshd.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sshd.sh b/assembly/src/main/distribution/bin/sshd.sh
index d07e203..a7b21ed 100644
--- a/assembly/src/main/distribution/bin/sshd.sh
+++ b/assembly/src/main/distribution/bin/sshd.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.server.SshServer "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.server.SshServerMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
index 14737bb..c0067be 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
@@ -34,5 +34,4 @@ public abstract class CliSupport {
         stderr.println(message);
         return true;
     }
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
new file mode 100644
index 0000000..903128a
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.server;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.ServiceLoader;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.sshd.cli.CliSupport;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.ServerAuthenticationManager;
+import org.apache.sshd.server.ServerFactoryManager;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.config.SshServerConfigFileReader;
+import org.apache.sshd.server.forward.ForwardingFilter;
+import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.server.subsystem.SubsystemFactory;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class SshServerCliSupport extends CliSupport {
+    protected SshServerCliSupport() {
+        super();
+    }
+
+    public static KeyPairProvider setupServerKeys(SshServer sshd, String hostKeyType, int hostKeySize, Collection<String> keyFiles) throws Exception {
+        if (GenericUtils.isEmpty(keyFiles)) {
+            AbstractGeneratorHostKeyProvider hostKeyProvider;
+            Path hostKeyFile;
+            if (SecurityUtils.isBouncyCastleRegistered()) {
+                hostKeyFile = new File("key.pem").toPath();
+                hostKeyProvider = SecurityUtils.createGeneratorHostKeyProvider(hostKeyFile);
+            } else {
+                hostKeyFile = new File("key.ser").toPath();
+                hostKeyProvider = new SimpleGeneratorHostKeyProvider(hostKeyFile);
+            }
+            hostKeyProvider.setAlgorithm(hostKeyType);
+            if (hostKeySize != 0) {
+                hostKeyProvider.setKeySize(hostKeySize);
+            }
+
+            List<KeyPair> keys = ValidateUtils.checkNotNullAndNotEmpty(hostKeyProvider.loadKeys(),
+                    "Failed to load keys from %s", hostKeyFile);
+            KeyPair kp = keys.get(0);
+            PublicKey pubKey = kp.getPublic();
+            String keyAlgorithm = pubKey.getAlgorithm();
+            if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyAlgorithm)) {
+                keyAlgorithm = KeyUtils.EC_ALGORITHM;
+            } else if (BuiltinIdentities.Constants.ED25519.equals(keyAlgorithm)) {
+                keyAlgorithm = SecurityUtils.EDDSA;
+                // TODO change the hostKeyProvider to one that supports read/write of EDDSA keys - see SSHD-703
+            }
+
+            // force re-generation of host key if not same algorithm
+            if (!Objects.equals(keyAlgorithm, hostKeyType)) {
+                Files.deleteIfExists(hostKeyFile);
+                hostKeyProvider.clearLoadedKeys();
+            }
+
+            return hostKeyProvider;
+        } else {
+            List<KeyPair> pairs = new ArrayList<>(keyFiles.size());
+            for (String keyFilePath : keyFiles) {
+                Path path = Paths.get(keyFilePath);
+                try (InputStream inputStream = Files.newInputStream(path)) {
+                    KeyPair kp = SecurityUtils.loadKeyPairIdentity(keyFilePath, inputStream, null);
+                    pairs.add(kp);
+                } catch (Exception e) {
+                    System.err.append("Failed (" + e.getClass().getSimpleName() + ")"
+                                + " to load host key file=" + keyFilePath
+                                + ": " + e.getMessage());
+                    throw e;
+                }
+            }
+
+            return new MappedKeyPairProvider(pairs);
+        }
+    }
+
+    public static ForwardingFilter setupServerForwarding(SshServer server, PropertyResolver options) {
+        ForwardingFilter forwardFilter = SshServerConfigFileReader.resolveServerForwarding(options);
+        server.setForwardingFilter(forwardFilter);
+        return forwardFilter;
+    }
+
+    public static Object setupServerBanner(ServerFactoryManager server, PropertyResolver options) {
+        Object banner = SshServerConfigFileReader.resolveBanner(options);
+        PropertyResolverUtils.updateProperty(server, ServerAuthenticationManager.WELCOME_BANNER, banner);
+        return banner;
+    }
+
+    public static List<NamedFactory<Command>> setupServerSubsystems(SshServer server, PropertyResolver options) {
+        ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(SubsystemFactory.class);
+        String classList = System.getProperty(SubsystemFactory.class.getName());
+        if (GenericUtils.isNotEmpty(classList)) {
+            String[] classes = GenericUtils.split(classList, ',');
+            List<NamedFactory<Command>> subsystems = new ArrayList<>(classes.length);
+            for (String fqcn : classes) {
+                try {
+                    Class<?> clazz = cl.loadClass(fqcn);
+                    SubsystemFactory factory = (SubsystemFactory) clazz.newInstance();
+                    subsystems.add(factory);
+                } catch (Throwable t) {
+                    System.err.append("Failed (").append(t.getClass().getSimpleName()).append(')')
+                        .append(" to instantiate subsystem=").append(fqcn)
+                        .append(": ").println(t.getMessage());
+                    System.err.flush();
+                    throw GenericUtils.toRuntimeException(t, true);
+                }
+            }
+
+            return subsystems;
+        }
+
+        String nameList = (options == null) ? null : options.getString(SshConfigFileReader.SUBSYSTEM_CONFIG_PROP);
+        if ("none".equalsIgnoreCase(nameList)) {
+            return Collections.emptyList();
+        }
+
+        boolean havePreferences = GenericUtils.isNotEmpty(nameList);
+        Collection<String> preferredNames = (!havePreferences)
+            ? Collections.emptySet()
+            : Stream.of(GenericUtils.split(nameList, ','))
+                .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)));
+        ServiceLoader<SubsystemFactory> loader = ServiceLoader.load(SubsystemFactory.class, cl);
+        List<NamedFactory<Command>> subsystems = new ArrayList<>();
+        for (SubsystemFactory factory : loader) {
+            String name = factory.getName();
+            if (havePreferences && (!preferredNames.contains(name))) {
+                continue;
+            }
+
+            subsystems.add(factory);
+        }
+
+        return subsystems;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
new file mode 100644
index 0000000..78e43a9
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.server;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
+import org.apache.sshd.common.io.IoServiceFactory;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
+import org.apache.sshd.server.config.keys.ServerIdentity;
+import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
+import org.apache.sshd.server.shell.ProcessShellFactory;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SshServerMain extends SshServerCliSupport {
+    public SshServerMain() {
+        super();    // in case someone wants to extend it
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static void main(String[] args) throws Exception {
+        int port = 8000;
+        String provider;
+        boolean error = false;
+        String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM;
+        int hostKeySize = 0;
+        Collection<String> keyFiles = null;
+        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+        int numArgs = GenericUtils.length(args);
+        for (int i = 0; i < numArgs; i++) {
+            String argName = args[i];
+            if ("-p".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+                port = Integer.parseInt(args[++i]);
+            } else if ("-key-type".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+
+                if (keyFiles != null) {
+                    System.err.println("option conflicts with -key-file: " + argName);
+                    error = true;
+                    break;
+                }
+                hostKeyType = args[++i].toUpperCase();
+            } else if ("-key-size".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+
+                if (keyFiles != null) {
+                    System.err.println("option conflicts with -key-file: " + argName);
+                    error = true;
+                    break;
+                }
+
+                hostKeySize = Integer.parseInt(args[++i]);
+            } else if ("-key-file".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+
+                String keyFilePath = args[++i];
+                if (keyFiles == null) {
+                    keyFiles = new LinkedList<>();
+                }
+                keyFiles.add(keyFilePath);
+            } else if ("-io".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+                provider = args[++i];
+                if ("mina".equals(provider)) {
+                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.MINA.getFactoryClassName());
+                } else if ("nio2".endsWith(provider)) {
+                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.NIO2.getFactoryClassName());
+                } else {
+                    System.err.println("provider should be mina or nio2: " + argName);
+                    error = true;
+                    break;
+                }
+            } else if ("-o".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires and argument: " + argName);
+                    error = true;
+                    break;
+                }
+
+                String opt = args[++i];
+                int idx = opt.indexOf('=');
+                if (idx <= 0) {
+                    System.err.println("bad syntax for option: " + opt);
+                    error = true;
+                    break;
+                }
+
+                String optName = opt.substring(0, idx);
+                String optValue = opt.substring(idx + 1);
+                if (ServerIdentity.HOST_KEY_CONFIG_PROP.equals(optName)) {
+                    if (keyFiles == null) {
+                        keyFiles = new LinkedList<>();
+                    }
+                    keyFiles.add(optValue);
+                } else if (SshConfigFileReader.PORT_CONFIG_PROP.equals(optName)) {
+                    port = Integer.parseInt(optValue);
+                } else {
+                    options.put(optName, optValue);
+                }
+            } else if (argName.startsWith("-")) {
+                System.err.println("illegal option: " + argName);
+                error = true;
+                break;
+            } else {
+                System.err.println("extra argument: " + argName);
+                error = true;
+                break;
+            }
+        }
+        if (error) {
+            System.err.println("usage: sshd [-p port] [-io mina|nio2] [-key-type RSA|DSA|EC] [-key-size NNNN] [-key-file <path>] [-o option=value]");
+            System.exit(-1);
+        }
+
+        SshServer sshd = SshServer.setUpDefaultServer();
+        Map<String, Object> props = sshd.getProperties();
+        props.putAll(options);
+
+        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
+        KeyPairProvider hostKeyProvider = setupServerKeys(sshd, hostKeyType, hostKeySize, keyFiles);
+        sshd.setKeyPairProvider(hostKeyProvider);
+        // Should come AFTER key pair provider setup so auto-welcome can be generated if needed
+        setupServerBanner(sshd, resolver);
+        sshd.setPort(port);
+
+        String macsOverride = resolver.getString(SshConfigFileReader.MACS_CONFIG_PROP);
+        if (GenericUtils.isNotEmpty(macsOverride)) {
+            SshConfigFileReader.configureMacs(sshd, macsOverride, true, true);
+        }
+
+        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
+        sshd.setPasswordAuthenticator((username, password, session) -> Objects.equals(username, password));
+        sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
+        setupServerForwarding(sshd, resolver);
+        sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(
+            command -> new ProcessShellFactory(GenericUtils.split(command, ' ')).create()
+        ).build());
+
+        List<NamedFactory<Command>> subsystems = setupServerSubsystems(sshd, resolver);
+        if (GenericUtils.isNotEmpty(subsystems)) {
+            System.out.append("Setup subsystems=").println(NamedResource.getNames(subsystems));
+            sshd.setSubsystemFactories(subsystems);
+        }
+
+        System.err.println("Starting SSHD on port " + port);
+        sshd.start();
+        Thread.sleep(Long.MAX_VALUE);
+        System.err.println("Exiting after a very (very very) long time");
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshFsMounter.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshFsMounter.java b/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshFsMounter.java
new file mode 100644
index 0000000..aad7cd5
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshFsMounter.java
@@ -0,0 +1,328 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
+import org.apache.sshd.common.io.IoServiceFactory;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
+import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.Utils;
+
+/**
+ * A basic implementation to allow remote mounting of the local file system via SFTP
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SshFsMounter extends SshServerCliSupport {
+    public static class MounterCommand extends AbstractLoggingBean implements Command, SessionAware, Runnable {
+        private final String command;
+        private final String cmdName;
+        private final List<String> args;
+        private String username;
+        private InputStream stdin;
+        private PrintStream stdout;
+        private PrintStream stderr;
+        private ExitCallback callback;
+        private ExecutorService executor;
+        private Future<?> future;
+
+        public MounterCommand(String command) {
+            this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
+
+            String[] comps = GenericUtils.split(this.command, ' ');
+            int numComps = GenericUtils.length(comps);
+            cmdName = GenericUtils.trimToEmpty(ValidateUtils.checkNotNullAndNotEmpty(comps[0], "No command name"));
+            if (numComps > 1) {
+                args = new ArrayList<>(numComps - 1);
+                for (int index = 1; index < numComps; index++) {
+                    String c = GenericUtils.trimToEmpty(comps[index]);
+                    if (GenericUtils.isEmpty(c)) {
+                        continue;
+                    }
+
+                    args.add(c);
+                }
+            } else {
+                args = Collections.emptyList();
+            }
+
+            log.info("<init>(" + command + ")");
+        }
+
+        @Override
+        public void run() {
+            try {
+                log.info("run(" + username + ")[" + command + "] start");
+                if ("id".equals(cmdName)) {
+                    int numArgs = GenericUtils.size(args);
+                    if (numArgs <= 0) {
+                        stdout.println("uid=0(root) gid=0(root) groups=0(root)");
+                    } else if (numArgs == 1) {
+                        String modifier = args.get(0);
+                        if ("-u".equals(modifier) || "-G".equals(modifier)) {
+                            stdout.println("0");
+                        } else {
+                            throw new IllegalArgumentException("Unknown modifier: " + modifier);
+                        }
+                    } else {
+                        throw new IllegalArgumentException("Unexpected extra command arguments");
+                    }
+                } else {
+                    throw new UnsupportedOperationException("Unknown command");
+                }
+
+                log.info("run(" + username + ")[" + command + "] end");
+                callback.onExit(0);
+            } catch (Exception e) {
+                log.error("run(" + username + ")[" + command + "] " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
+                stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
+                callback.onExit(-1, e.toString());
+            }
+        }
+
+        @Override
+        public void setSession(ServerSession session) {
+            username = session.getUsername();
+        }
+
+        @Override
+        public void setInputStream(InputStream in) {
+            this.stdin = in;
+        }
+
+        @Override
+        public void setOutputStream(OutputStream out) {
+            this.stdout = new PrintStream(out, true);
+        }
+
+        @Override
+        public void setErrorStream(OutputStream err) {
+            this.stderr = new PrintStream(err, true);
+        }
+
+        @Override
+        public void setExitCallback(ExitCallback callback) {
+            this.callback = callback;
+        }
+
+        @Override
+        public void start(Environment env) throws IOException {
+            executor = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
+            future = executor.submit(this);
+        }
+
+        @Override
+        public void destroy() {
+            stopCommand();
+
+            if (stdout != null) {
+                try {
+                    log.info("destroy(" + username + ")[" + command + "] close stdout");
+                    stdout.close();
+                    log.info("destroy(" + username + ")[" + command + "] stdout closed");
+                } finally {
+                    stdout = null;
+                }
+            }
+
+            if (stderr != null) {
+                try {
+                    log.info("destroy(" + username + ")[" + command + "] close stderr");
+                    stderr.close();
+                    log.info("destroy(" + username + ")[" + command + "] stderr closed");
+                } finally {
+                    stderr = null;
+                }
+            }
+
+            if (stdin != null) {
+                try {
+                    log.info("destroy(" + username + ")[" + command + "] close stdin");
+                    stdin.close();
+                    log.info("destroy(" + username + ")[" + command + "] stdin closed");
+                } catch (IOException e) {
+                    log.warn("destroy(" + username + ")[" + command + "] failed (" + e.getClass().getSimpleName() + ") to close stdin: " + e.getMessage());
+                    if (log.isDebugEnabled()) {
+                        log.debug("destroy(" + username + ")[" + command + "] failure details", e);
+                    }
+                } finally {
+                    stdin = null;
+                }
+            }
+        }
+
+        private void stopCommand() {
+            if ((future != null) && (!future.isDone())) {
+                try {
+                    log.info("stopCommand(" + username + ")[" + command + "] cancelling");
+                    future.cancel(true);
+                    log.info("stopCommand(" + username + ")[" + command + "] cancelled");
+                } finally {
+                    future = null;
+                }
+            }
+
+            if ((executor != null) && (!executor.isShutdown())) {
+                try {
+                    log.info("stopCommand(" + username + ")[" + command + "] shutdown executor");
+                    executor.shutdownNow();
+                    log.info("stopCommand(" + username + ")[" + command + "] executor shut down");
+                } finally {
+                    executor = null;
+                }
+            }
+        }
+    }
+
+    public static class MounterCommandFactory implements CommandFactory {
+        public static final MounterCommandFactory INSTANCE = new MounterCommandFactory();
+
+        public MounterCommandFactory() {
+            super();
+        }
+
+        @Override
+        public Command createCommand(String command) {
+            return new MounterCommand(command);
+        }
+    }
+
+    private SshFsMounter() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static void main(String[] args) throws Exception {
+        int port = SshConfigFileReader.DEFAULT_PORT;
+        boolean error = false;
+        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        int numArgs = GenericUtils.length(args);
+        for (int i = 0; i < numArgs; i++) {
+            String argName = args[i];
+            if ("-p".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    break;
+                }
+                port = Integer.parseInt(args[++i]);
+            } else if ("-io".equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
+                    break;
+                }
+
+                String provider = args[++i];
+                if ("mina".equals(provider)) {
+                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.MINA.getFactoryClassName());
+                } else if ("nio2".endsWith(provider)) {
+                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.NIO2.getFactoryClassName());
+                } else {
+                    System.err.println("provider should be mina or nio2: " + argName);
+                    error = true;
+                    break;
+                }
+            } else if ("-o".equals(argName)) {
+                if ((i + 1) >= numArgs) {
+                    System.err.println("option requires and argument: " + argName);
+                    error = true;
+                    break;
+                }
+                String opt = args[++i];
+                int idx = opt.indexOf('=');
+                if (idx <= 0) {
+                    System.err.println("bad syntax for option: " + opt);
+                    error = true;
+                    break;
+                }
+                options.put(opt.substring(0, idx), opt.substring(idx + 1));
+            } else if (argName.startsWith("-")) {
+                System.err.println("illegal option: " + argName);
+                error = true;
+                break;
+            } else {
+                System.err.println("extra argument: " + argName);
+                error = true;
+                break;
+            }
+        }
+        if (error) {
+            System.err.println("usage: sshfs [-p port] [-io mina|nio2] [-o option=value]");
+            System.exit(-1);
+        }
+
+        SshServer sshd = Utils.setupTestServer(SshFsMounter.class);
+        Map<String, Object> props = sshd.getProperties();
+        props.putAll(options);
+        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
+        File targetFolder = Objects.requireNonNull(Utils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder");
+        if (SecurityUtils.isBouncyCastleRegistered()) {
+            sshd.setKeyPairProvider(SecurityUtils.createGeneratorHostKeyProvider(new File(targetFolder, "key.pem").toPath()));
+        } else {
+            sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File(targetFolder, "key.ser")));
+        }
+        // Should come AFTER key pair provider setup so auto-welcome can be generated if needed
+        setupServerBanner(sshd, resolver);
+
+        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
+        sshd.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
+        sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
+        sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(MounterCommandFactory.INSTANCE).build());
+        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
+        sshd.setPort(port);
+
+        System.err.println("Starting SSHD on port " + port);
+        sshd.start();
+        Thread.sleep(Long.MAX_VALUE);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshServerMainDevelopment.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshServerMainDevelopment.java b/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshServerMainDevelopment.java
new file mode 100644
index 0000000..28409fa
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/server/SshServerMainDevelopment.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.sshd.cli.server;
+
+/**
+ * Just a test class used to invoke {@link SshServerMain#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class SshServerMainDevelopment {
+    private SshServerMainDevelopment() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static void main(String[] args) throws Exception {
+        SshServerMain.main(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
index 255668d..f93c5d0 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
@@ -18,69 +18,41 @@
  */
 package org.apache.sshd.server;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.KeyPair;
-import java.security.PublicKey;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.sshd.common.Closeable;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.PropertyResolver;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.ServiceFactory;
-import org.apache.sshd.common.config.SshConfigFileReader;
-import org.apache.sshd.common.config.keys.BuiltinIdentities;
-import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.helpers.AbstractFactoryManager;
-import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoServiceFactory;
 import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.server.auth.UserAuth;
 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
 import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;
 import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
 import org.apache.sshd.server.auth.password.PasswordAuthenticator;
-import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
-import org.apache.sshd.server.config.SshServerConfigFileReader;
-import org.apache.sshd.server.config.keys.ServerIdentity;
-import org.apache.sshd.server.forward.ForwardingFilter;
-import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
-import org.apache.sshd.server.scp.ScpCommandFactory;
 import org.apache.sshd.server.session.ServerConnectionServiceFactory;
 import org.apache.sshd.server.session.ServerProxyAcceptor;
 import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
 import org.apache.sshd.server.session.SessionFactory;
-import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
-import org.apache.sshd.server.shell.ProcessShellFactory;
 
 /**
  * <p>
@@ -426,216 +398,4 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa
     public static SshServer setUpDefaultServer() {
         return ServerBuilder.builder().build();
     }
-
-    /*=================================
-          Main class implementation
-     *=================================*/
-
-    public static KeyPairProvider setupServerKeys(SshServer sshd, String hostKeyType, int hostKeySize, Collection<String> keyFiles) throws Exception {
-        if (GenericUtils.isEmpty(keyFiles)) {
-            AbstractGeneratorHostKeyProvider hostKeyProvider;
-            Path hostKeyFile;
-            if (SecurityUtils.isBouncyCastleRegistered()) {
-                hostKeyFile = new File("key.pem").toPath();
-                hostKeyProvider = SecurityUtils.createGeneratorHostKeyProvider(hostKeyFile);
-            } else {
-                hostKeyFile = new File("key.ser").toPath();
-                hostKeyProvider = new SimpleGeneratorHostKeyProvider(hostKeyFile);
-            }
-            hostKeyProvider.setAlgorithm(hostKeyType);
-            if (hostKeySize != 0) {
-                hostKeyProvider.setKeySize(hostKeySize);
-            }
-
-            List<KeyPair> keys = ValidateUtils.checkNotNullAndNotEmpty(hostKeyProvider.loadKeys(),
-                    "Failed to load keys from %s", hostKeyFile);
-            KeyPair kp = keys.get(0);
-            PublicKey pubKey = kp.getPublic();
-            String keyAlgorithm = pubKey.getAlgorithm();
-            if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyAlgorithm)) {
-                keyAlgorithm = KeyUtils.EC_ALGORITHM;
-            } else if (BuiltinIdentities.Constants.ED25519.equals(keyAlgorithm)) {
-                keyAlgorithm = SecurityUtils.EDDSA;
-                // TODO change the hostKeyProvider to one that supports read/write of EDDSA keys - see SSHD-703
-            }
-
-            // force re-generation of host key if not same algorithm
-            if (!Objects.equals(keyAlgorithm, hostKeyType)) {
-                Files.deleteIfExists(hostKeyFile);
-                hostKeyProvider.clearLoadedKeys();
-            }
-
-            return hostKeyProvider;
-        } else {
-            List<KeyPair> pairs = new ArrayList<>(keyFiles.size());
-            for (String keyFilePath : keyFiles) {
-                Path path = Paths.get(keyFilePath);
-                try (InputStream inputStream = Files.newInputStream(path)) {
-                    KeyPair kp = SecurityUtils.loadKeyPairIdentity(keyFilePath, inputStream, null);
-                    pairs.add(kp);
-                } catch (Exception e) {
-                    System.err.append("Failed (" + e.getClass().getSimpleName() + ")"
-                                + " to load host key file=" + keyFilePath
-                                + ": " + e.getMessage());
-                    throw e;
-                }
-            }
-
-            return new MappedKeyPairProvider(pairs);
-        }
-    }
-
-    public static void main(String[] args) throws Exception {
-        int port = 8000;
-        String provider;
-        boolean error = false;
-        String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM;
-        int hostKeySize = 0;
-        Collection<String> keyFiles = null;
-        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
-        int numArgs = GenericUtils.length(args);
-        for (int i = 0; i < numArgs; i++) {
-            String argName = args[i];
-            if ("-p".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    error = true;
-                    break;
-                }
-                port = Integer.parseInt(args[++i]);
-            } else if ("-key-type".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    error = true;
-                    break;
-                }
-
-                if (keyFiles != null) {
-                    System.err.println("option conflicts with -key-file: " + argName);
-                    error = true;
-                    break;
-                }
-                hostKeyType = args[++i].toUpperCase();
-            } else if ("-key-size".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    error = true;
-                    break;
-                }
-
-                if (keyFiles != null) {
-                    System.err.println("option conflicts with -key-file: " + argName);
-                    error = true;
-                    break;
-                }
-
-                hostKeySize = Integer.parseInt(args[++i]);
-            } else if ("-key-file".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    error = true;
-                    break;
-                }
-
-                String keyFilePath = args[++i];
-                if (keyFiles == null) {
-                    keyFiles = new LinkedList<>();
-                }
-                keyFiles.add(keyFilePath);
-            } else if ("-io".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    error = true;
-                    break;
-                }
-                provider = args[++i];
-                if ("mina".equals(provider)) {
-                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.MINA.getFactoryClassName());
-                } else if ("nio2".endsWith(provider)) {
-                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.NIO2.getFactoryClassName());
-                } else {
-                    System.err.println("provider should be mina or nio2: " + argName);
-                    error = true;
-                    break;
-                }
-            } else if ("-o".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires and argument: " + argName);
-                    error = true;
-                    break;
-                }
-                String opt = args[++i];
-                int idx = opt.indexOf('=');
-                if (idx <= 0) {
-                    System.err.println("bad syntax for option: " + opt);
-                    error = true;
-                    break;
-                }
-
-                String optName = opt.substring(0, idx);
-                String optValue = opt.substring(idx + 1);
-                if (ServerIdentity.HOST_KEY_CONFIG_PROP.equals(optName)) {
-                    if (keyFiles == null) {
-                        keyFiles = new LinkedList<>();
-                    }
-                    keyFiles.add(optValue);
-                } else {
-                    options.put(optName, optValue);
-                }
-            } else if (argName.startsWith("-")) {
-                System.err.println("illegal option: " + argName);
-                error = true;
-                break;
-            } else {
-                System.err.println("extra argument: " + argName);
-                error = true;
-                break;
-            }
-        }
-        if (error) {
-            System.err.println("usage: sshd [-p port] [-io mina|nio2] [-key-type RSA|DSA|EC] [-key-size NNNN] [-key-file <path>] [-o option=value]");
-            System.exit(-1);
-        }
-
-        SshServer sshd = SshServer.setUpDefaultServer();
-        Map<String, Object> props = sshd.getProperties();
-        props.putAll(options);
-
-        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
-        KeyPairProvider hostKeyProvider = setupServerKeys(sshd, hostKeyType, hostKeySize, keyFiles);
-        sshd.setKeyPairProvider(hostKeyProvider);
-        // Should come AFTER key pair provider setup so auto-welcome can be generated if needed
-        setupServerBanner(sshd, resolver);
-        sshd.setPort(port);
-
-        String macsOverride = resolver.getString(SshConfigFileReader.MACS_CONFIG_PROP);
-        if (GenericUtils.isNotEmpty(macsOverride)) {
-            SshConfigFileReader.configureMacs(sshd, macsOverride, true, true);
-        }
-
-        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
-        sshd.setPasswordAuthenticator((username, password, session) -> Objects.equals(username, password));
-        sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
-        setupServerForwarding(sshd, resolver);
-        sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(
-            command -> new ProcessShellFactory(GenericUtils.split(command, ' ')).create()
-        ).build());
-
-        System.err.println("Starting SSHD on port " + port);
-        sshd.start();
-        Thread.sleep(Long.MAX_VALUE);
-    }
-
-    public static ForwardingFilter setupServerForwarding(SshServer server, PropertyResolver options) {
-        ForwardingFilter forwardFilter = SshServerConfigFileReader.resolveServerForwarding(options);
-        server.setForwardingFilter(forwardFilter);
-        return forwardFilter;
-    }
-
-    public static Object setupServerBanner(ServerFactoryManager server, PropertyResolver options) {
-        Object banner = SshServerConfigFileReader.resolveBanner(options);
-        PropertyResolverUtils.updateProperty(server, ServerAuthenticationManager.WELCOME_BANNER, banner);
-        return banner;
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-core/src/test/java/org/apache/sshd/server/SshServerMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/SshServerMain.java b/sshd-core/src/test/java/org/apache/sshd/server/SshServerMain.java
deleted file mode 100644
index d4f3c60..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/SshServerMain.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.server;
-
-/**
- * Just a test class used to invoke {@link SshServer#main(String[])} in
- * order to have logging - which is in {@code test} scope
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class SshServerMain {
-    private SshServerMain() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    public static void main(String[] args) throws Exception {
-        SshServer.main(args);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-sftp/src/main/filtered-resources/META-INF/services/org.apache.sshd.server.subsystem.SubsystemFactory
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/filtered-resources/META-INF/services/org.apache.sshd.server.subsystem.SubsystemFactory b/sshd-sftp/src/main/filtered-resources/META-INF/services/org.apache.sshd.server.subsystem.SubsystemFactory
new file mode 100644
index 0000000..aa3de97
--- /dev/null
+++ b/sshd-sftp/src/main/filtered-resources/META-INF/services/org.apache.sshd.server.subsystem.SubsystemFactory
@@ -0,0 +1,20 @@
+##
+## 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.
+##
+
+org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2b7e22a3/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java b/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java
deleted file mode 100644
index e6b10e0..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.sshd.server.subsystem.sftp;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-import org.apache.sshd.common.PropertyResolver;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.config.SshConfigFileReader;
-import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
-import org.apache.sshd.common.io.IoServiceFactory;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.common.util.security.SecurityUtils;
-import org.apache.sshd.common.util.threads.ThreadUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
-import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
-import org.apache.sshd.util.test.Utils;
-
-/**
- * A basic implementation to allow remote mounting of the local file system via SFTP
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public final class SshFsMounter {
-    public static class MounterCommand extends AbstractLoggingBean implements Command, SessionAware, Runnable {
-        private final String command;
-        private final String cmdName;
-        private final List<String> args;
-        private String username;
-        private InputStream stdin;
-        private PrintStream stdout;
-        private PrintStream stderr;
-        private ExitCallback callback;
-        private ExecutorService executor;
-        private Future<?> future;
-
-        public MounterCommand(String command) {
-            this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
-
-            String[] comps = GenericUtils.split(this.command, ' ');
-            int numComps = GenericUtils.length(comps);
-            cmdName = GenericUtils.trimToEmpty(ValidateUtils.checkNotNullAndNotEmpty(comps[0], "No command name"));
-            if (numComps > 1) {
-                args = new ArrayList<>(numComps - 1);
-                for (int index = 1; index < numComps; index++) {
-                    String c = GenericUtils.trimToEmpty(comps[index]);
-                    if (GenericUtils.isEmpty(c)) {
-                        continue;
-                    }
-
-                    args.add(c);
-                }
-            } else {
-                args = Collections.emptyList();
-            }
-
-            log.info("<init>(" + command + ")");
-        }
-
-        @Override
-        public void run() {
-            try {
-                log.info("run(" + username + ")[" + command + "] start");
-                if ("id".equals(cmdName)) {
-                    int numArgs = GenericUtils.size(args);
-                    if (numArgs <= 0) {
-                        stdout.println("uid=0(root) gid=0(root) groups=0(root)");
-                    } else if (numArgs == 1) {
-                        String modifier = args.get(0);
-                        if ("-u".equals(modifier) || "-G".equals(modifier)) {
-                            stdout.println("0");
-                        } else {
-                            throw new IllegalArgumentException("Unknown modifier: " + modifier);
-                        }
-                    } else {
-                        throw new IllegalArgumentException("Unexpected extra command arguments");
-                    }
-                } else {
-                    throw new UnsupportedOperationException("Unknown command");
-                }
-
-                log.info("run(" + username + ")[" + command + "] end");
-                callback.onExit(0);
-            } catch (Exception e) {
-                log.error("run(" + username + ")[" + command + "] " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
-                stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
-                callback.onExit(-1, e.toString());
-            }
-        }
-
-        @Override
-        public void setSession(ServerSession session) {
-            username = session.getUsername();
-        }
-
-        @Override
-        public void setInputStream(InputStream in) {
-            this.stdin = in;
-        }
-
-        @Override
-        public void setOutputStream(OutputStream out) {
-            this.stdout = new PrintStream(out, true);
-        }
-
-        @Override
-        public void setErrorStream(OutputStream err) {
-            this.stderr = new PrintStream(err, true);
-        }
-
-        @Override
-        public void setExitCallback(ExitCallback callback) {
-            this.callback = callback;
-        }
-
-        @Override
-        public void start(Environment env) throws IOException {
-            executor = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
-            future = executor.submit(this);
-        }
-
-        @Override
-        public void destroy() {
-            stopCommand();
-
-            if (stdout != null) {
-                try {
-                    log.info("destroy(" + username + ")[" + command + "] close stdout");
-                    stdout.close();
-                    log.info("destroy(" + username + ")[" + command + "] stdout closed");
-                } finally {
-                    stdout = null;
-                }
-            }
-
-            if (stderr != null) {
-                try {
-                    log.info("destroy(" + username + ")[" + command + "] close stderr");
-                    stderr.close();
-                    log.info("destroy(" + username + ")[" + command + "] stderr closed");
-                } finally {
-                    stderr = null;
-                }
-            }
-
-            if (stdin != null) {
-                try {
-                    log.info("destroy(" + username + ")[" + command + "] close stdin");
-                    stdin.close();
-                    log.info("destroy(" + username + ")[" + command + "] stdin closed");
-                } catch (IOException e) {
-                    log.warn("destroy(" + username + ")[" + command + "] failed (" + e.getClass().getSimpleName() + ") to close stdin: " + e.getMessage());
-                    if (log.isDebugEnabled()) {
-                        log.debug("destroy(" + username + ")[" + command + "] failure details", e);
-                    }
-                } finally {
-                    stdin = null;
-                }
-            }
-        }
-
-        private void stopCommand() {
-            if ((future != null) && (!future.isDone())) {
-                try {
-                    log.info("stopCommand(" + username + ")[" + command + "] cancelling");
-                    future.cancel(true);
-                    log.info("stopCommand(" + username + ")[" + command + "] cancelled");
-                } finally {
-                    future = null;
-                }
-            }
-
-            if ((executor != null) && (!executor.isShutdown())) {
-                try {
-                    log.info("stopCommand(" + username + ")[" + command + "] shutdown executor");
-                    executor.shutdownNow();
-                    log.info("stopCommand(" + username + ")[" + command + "] executor shut down");
-                } finally {
-                    executor = null;
-                }
-            }
-        }
-    }
-
-    public static class MounterCommandFactory implements CommandFactory {
-        public static final MounterCommandFactory INSTANCE = new MounterCommandFactory();
-
-        public MounterCommandFactory() {
-            super();
-        }
-
-        @Override
-        public Command createCommand(String command) {
-            return new MounterCommand(command);
-        }
-    }
-
-    private SshFsMounter() {
-        throw new UnsupportedOperationException("No instance");
-    }
-
-    //////////////////////////////////////////////////////////////////////////
-
-    public static void main(String[] args) throws Exception {
-        int port = SshConfigFileReader.DEFAULT_PORT;
-        boolean error = false;
-        Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        int numArgs = GenericUtils.length(args);
-        for (int i = 0; i < numArgs; i++) {
-            String argName = args[i];
-            if ("-p".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    break;
-                }
-                port = Integer.parseInt(args[++i]);
-            } else if ("-io".equals(argName)) {
-                if (i + 1 >= numArgs) {
-                    System.err.println("option requires an argument: " + argName);
-                    break;
-                }
-
-                String provider = args[++i];
-                if ("mina".equals(provider)) {
-                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.MINA.getFactoryClassName());
-                } else if ("nio2".endsWith(provider)) {
-                    System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.NIO2.getFactoryClassName());
-                } else {
-                    System.err.println("provider should be mina or nio2: " + argName);
-                    error = true;
-                    break;
-                }
-            } else if ("-o".equals(argName)) {
-                if ((i + 1) >= numArgs) {
-                    System.err.println("option requires and argument: " + argName);
-                    error = true;
-                    break;
-                }
-                String opt = args[++i];
-                int idx = opt.indexOf('=');
-                if (idx <= 0) {
-                    System.err.println("bad syntax for option: " + opt);
-                    error = true;
-                    break;
-                }
-                options.put(opt.substring(0, idx), opt.substring(idx + 1));
-            } else if (argName.startsWith("-")) {
-                System.err.println("illegal option: " + argName);
-                error = true;
-                break;
-            } else {
-                System.err.println("extra argument: " + argName);
-                error = true;
-                break;
-            }
-        }
-        if (error) {
-            System.err.println("usage: sshfs [-p port] [-io mina|nio2] [-o option=value]");
-            System.exit(-1);
-        }
-
-        SshServer sshd = Utils.setupTestServer(SshFsMounter.class);
-        Map<String, Object> props = sshd.getProperties();
-        props.putAll(options);
-        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
-        File targetFolder = Objects.requireNonNull(Utils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder");
-        if (SecurityUtils.isBouncyCastleRegistered()) {
-            sshd.setKeyPairProvider(SecurityUtils.createGeneratorHostKeyProvider(new File(targetFolder, "key.pem").toPath()));
-        } else {
-            sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File(targetFolder, "key.ser")));
-        }
-        // Should come AFTER key pair provider setup so auto-welcome can be generated if needed
-        SshServer.setupServerBanner(sshd, resolver);
-
-        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
-        sshd.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
-        sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
-        sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(MounterCommandFactory.INSTANCE).build());
-        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-        sshd.setPort(port);
-
-        System.err.println("Starting SSHD on port " + port);
-        sshd.start();
-        Thread.sleep(Long.MAX_VALUE);
-    }
-}