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:06 UTC

[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

[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);
-    }
-}