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:09 UTC
[5/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for
client commands to sshd-cli module
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);
+ }
+}