blob: 129951804335fbe54036fdc24124cb22dfe27ced [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.client;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StreamCorruptedException;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.apache.sshd.agent.SshAgentFactory;
import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
import org.apache.sshd.client.auth.UserAuth;
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
import org.apache.sshd.client.config.hosts.KnownHostEntry;
import org.apache.sshd.client.config.keys.ClientIdentityLoader;
import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.future.DefaultConnectFuture;
import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.AbstractClientSession;
import org.apache.sshd.client.session.ClientConnectionServiceFactory;
import org.apache.sshd.client.session.ClientProxyConnector;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.ClientSessionCreator;
import org.apache.sshd.client.session.ClientUserAuthServiceFactory;
import org.apache.sshd.client.session.SessionFactory;
import org.apache.sshd.client.simple.AbstractSimpleClientSessionCreator;
import org.apache.sshd.client.simple.SimpleClient;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.Factory;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.ServiceFactory;
import org.apache.sshd.common.channel.Channel;
import org.apache.sshd.common.cipher.BuiltinCiphers;
import org.apache.sshd.common.cipher.Cipher;
import org.apache.sshd.common.compression.BuiltinCompressions;
import org.apache.sshd.common.compression.Compression;
import org.apache.sshd.common.config.CompressionConfigValue;
import org.apache.sshd.common.config.SshConfigFileReader;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.helpers.AbstractFactoryManager;
import org.apache.sshd.common.io.IoConnectFuture;
import org.apache.sshd.common.io.IoConnector;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.keyprovider.AbstractFileKeyPairProvider;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.mac.BuiltinMacs;
import org.apache.sshd.common.mac.Mac;
import org.apache.sshd.common.scp.ScpFileOpener;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.common.util.Supplier;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.NoCloseOutputStream;
import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
* <P>
* Entry point for the client side of the SSH protocol.
* </P>
*
* <P>
* The default configured client can be created using
* the {@link #setUpDefaultClient()}. The next step is to
* start the client using the {@link #start()} method.
* </P>
*
* <P>
* Sessions can then be created using on of the
* {@link #connect(String, String, int)} or {@link #connect(String, java.net.SocketAddress)}
* methods.
* </P>
*
* <P>
* The client can be stopped any time using the {@link #stop()} method.
* </P>
*
* <P>
* Following is an example of using the {@code SshClient}:
* </P>
*
* <pre>
* try (SshClient client = SshClient.setUpDefaultClient()) {
* client.start();
*
* try (ClientSession session = client.connect(login, host, port).await().getSession()) {
* session.addPasswordIdentity(password);
* session.auth().verify(...timeout...);
*
* try (ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_SHELL)) {
* channel.setIn(new NoCloseInputStream(System.in));
* channel.setOut(new NoCloseOutputStream(System.out));
* channel.setErr(new NoCloseOutputStream(System.err));
* channel.open();
* channel.waitFor(ClientChannel.CLOSED, 0);
* } finally {
* session.close(false);
* }
* } finally {
* client.stop();
* }
* }
* </pre>
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class SshClient extends AbstractFactoryManager implements ClientFactoryManager, ClientSessionCreator, Closeable {
public static final Factory<SshClient> DEFAULT_SSH_CLIENT_FACTORY = new Factory<SshClient>() {
@Override
public SshClient create() {
return new SshClient();
}
};
/**
* Command line option used to indicate non-default target port
*/
public static final String SSH_CLIENT_PORT_OPTION = "-p";
/**
* Default user authentication preferences if not set
* @see <A HREF="http://linux.die.net/man/5/ssh_config">ssh_config(5) - PreferredAuthentications</A>
*/
public static final List<NamedFactory<UserAuth>> DEFAULT_USER_AUTH_FACTORIES =
Collections.unmodifiableList(Arrays.<NamedFactory<UserAuth>>asList(
UserAuthPublicKeyFactory.INSTANCE,
UserAuthKeyboardInteractiveFactory.INSTANCE,
UserAuthPasswordFactory.INSTANCE
));
public static final List<ServiceFactory> DEFAULT_SERVICE_FACTORIES =
Collections.unmodifiableList(Arrays.asList(
ClientUserAuthServiceFactory.INSTANCE,
ClientConnectionServiceFactory.INSTANCE
));
protected IoConnector connector;
protected SessionFactory sessionFactory;
protected UserInteraction userInteraction;
protected List<NamedFactory<UserAuth>> userAuthFactories;
private ClientProxyConnector proxyConnector;
private ServerKeyVerifier serverKeyVerifier;
private HostConfigEntryResolver hostConfigEntryResolver;
private ClientIdentityLoader clientIdentityLoader;
private FilePasswordProvider filePasswordProvider;
private PasswordIdentityProvider passwordIdentityProvider;
private ScpFileOpener scpOpener;
private final List<Object> identities = new CopyOnWriteArrayList<>();
private final AuthenticationIdentitiesProvider identitiesProvider;
public SshClient() {
identitiesProvider = AuthenticationIdentitiesProvider.Utils.wrap(identities);
}
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public ClientProxyConnector getClientProxyConnector() {
return proxyConnector;
}
@Override
public void setClientProxyConnector(ClientProxyConnector proxyConnector) {
this.proxyConnector = proxyConnector;
}
@Override
public ScpFileOpener getScpFileOpener() {
return scpOpener;
}
@Override
public void setScpFileOpener(ScpFileOpener opener) {
scpOpener = opener;
}
@Override
public ServerKeyVerifier getServerKeyVerifier() {
return serverKeyVerifier;
}
@Override
public void setServerKeyVerifier(ServerKeyVerifier serverKeyVerifier) {
this.serverKeyVerifier = ValidateUtils.checkNotNull(serverKeyVerifier, "No server key verifier");
}
@Override
public HostConfigEntryResolver getHostConfigEntryResolver() {
return hostConfigEntryResolver;
}
@Override
public void setHostConfigEntryResolver(HostConfigEntryResolver resolver) {
this.hostConfigEntryResolver = ValidateUtils.checkNotNull(resolver, "No host configuration entry resolver");
}
@Override
public FilePasswordProvider getFilePasswordProvider() {
return filePasswordProvider;
}
@Override
public void setFilePasswordProvider(FilePasswordProvider provider) {
this.filePasswordProvider = ValidateUtils.checkNotNull(provider, "No file password provider");
}
@Override
public ClientIdentityLoader getClientIdentityLoader() {
return clientIdentityLoader;
}
@Override
public void setClientIdentityLoader(ClientIdentityLoader loader) {
this.clientIdentityLoader = ValidateUtils.checkNotNull(loader, "No client identity loader");
}
@Override
public UserInteraction getUserInteraction() {
return userInteraction;
}
@Override
public void setUserInteraction(UserInteraction userInteraction) {
this.userInteraction = userInteraction;
}
@Override
public List<NamedFactory<UserAuth>> getUserAuthFactories() {
return userAuthFactories;
}
@Override
public void setUserAuthFactories(List<NamedFactory<UserAuth>> userAuthFactories) {
this.userAuthFactories = ValidateUtils.checkNotNullAndNotEmpty(userAuthFactories, "No user auth factories");
}
@Override
public AuthenticationIdentitiesProvider getRegisteredIdentities() {
return identitiesProvider;
}
@Override
public PasswordIdentityProvider getPasswordIdentityProvider() {
return passwordIdentityProvider;
}
@Override
public void setPasswordIdentityProvider(PasswordIdentityProvider provider) {
passwordIdentityProvider = provider;
}
@Override
public void addPasswordIdentity(String password) {
identities.add(ValidateUtils.checkNotNullAndNotEmpty(password, "No password provided"));
if (log.isDebugEnabled()) { // don't show the password in the log
log.debug("addPasswordIdentity({}) {}", this, KeyUtils.getFingerPrint(password));
}
}
@Override
public String removePasswordIdentity(String password) {
if (GenericUtils.isEmpty(password)) {
return null;
}
int index = AuthenticationIdentitiesProvider.Utils.findIdentityIndex(
identities, AuthenticationIdentitiesProvider.Utils.PASSWORD_IDENTITY_COMPARATOR, password);
if (index >= 0) {
return (String) identities.remove(index);
} else {
return null;
}
}
@Override
public void addPublicKeyIdentity(KeyPair kp) {
ValidateUtils.checkNotNull(kp, "No key-pair to add");
ValidateUtils.checkNotNull(kp.getPublic(), "No public key");
ValidateUtils.checkNotNull(kp.getPrivate(), "No private key");
identities.add(kp);
if (log.isDebugEnabled()) {
log.debug("addPublicKeyIdentity({}) {}", this, KeyUtils.getFingerPrint(kp.getPublic()));
}
}
@Override
public KeyPair removePublicKeyIdentity(KeyPair kp) {
if (kp == null) {
return null;
}
int index = AuthenticationIdentitiesProvider.Utils.findIdentityIndex(
identities, AuthenticationIdentitiesProvider.Utils.KEYPAIR_IDENTITY_COMPARATOR, kp);
if (index >= 0) {
return (KeyPair) identities.remove(index);
} else {
return null;
}
}
@Override
protected void checkConfig() {
super.checkConfig();
ValidateUtils.checkNotNull(getTcpipForwarderFactory(), "TcpipForwarderFactory not set");
ValidateUtils.checkNotNull(getServerKeyVerifier(), "ServerKeyVerifier not set");
ValidateUtils.checkNotNull(getHostConfigEntryResolver(), "HostConfigEntryResolver not set");
ValidateUtils.checkNotNull(getClientIdentityLoader(), "ClientIdentityLoader not set");
ValidateUtils.checkNotNull(getFilePasswordProvider(), "FilePasswordProvider not set");
// if no client identities override use the default
KeyPairProvider defaultIdentities = getKeyPairProvider();
if (defaultIdentities == null) {
setKeyPairProvider(new DefaultClientIdentitiesWatcher(
new Supplier<ClientIdentityLoader>() {
@Override
public ClientIdentityLoader get() {
return getClientIdentityLoader();
}
},
new Supplier<FilePasswordProvider>() {
@Override
public FilePasswordProvider get() {
return getFilePasswordProvider();
}
}));
}
// Register the additional agent forwarding channel if needed
SshAgentFactory agentFactory = getAgentFactory();
if (agentFactory != null) {
List<NamedFactory<Channel>> factories = getChannelFactories();
if (GenericUtils.isEmpty(factories)) {
factories = new ArrayList<>();
} else {
factories = new ArrayList<>(factories);
}
factories.add(ValidateUtils.checkNotNull(agentFactory.getChannelForwardingFactory(), "No agent channel forwarding factory for %s", agentFactory));
setChannelFactories(factories);
}
if (GenericUtils.isEmpty(getServiceFactories())) {
setServiceFactories(DEFAULT_SERVICE_FACTORIES);
}
if (GenericUtils.isEmpty(getUserAuthFactories())) {
setUserAuthFactories(DEFAULT_USER_AUTH_FACTORIES);
}
}
public void start() {
checkConfig();
if (sessionFactory == null) {
sessionFactory = createSessionFactory();
}
setupSessionTimeout(sessionFactory);
connector = createConnector();
}
public void stop() {
try {
long maxWait = PropertyResolverUtils.getLongProperty(this, STOP_WAIT_TIME, DEFAULT_STOP_WAIT_TIME);
boolean successful = close(true).await(maxWait);
if (!successful) {
throw new SocketTimeoutException("Failed to receive closure confirmation within " + maxWait + " millis");
}
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(e.getClass().getSimpleName() + " while stopping client: " + e.getMessage());
}
if (log.isTraceEnabled()) {
log.trace("Stop exception details", e);
}
}
}
public void open() throws IOException {
start();
}
@Override
protected Closeable getInnerCloseable() {
return builder()
.run(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
removeSessionTimeout(sessionFactory);
}
})
.sequential(connector, ioServiceFactory)
.run(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
connector = null;
ioServiceFactory = null;
if (shutdownExecutor && (executor != null) && (!executor.isShutdown())) {
try {
executor.shutdownNow();
} finally {
executor = null;
}
}
}
})
.build();
}
@Override
public ConnectFuture connect(String username, String host, int port) throws IOException {
HostConfigEntryResolver resolver = getHostConfigEntryResolver();
HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, username);
if (entry == null) {
if (log.isDebugEnabled()) {
log.debug("connect({}@{}:{}) no overrides", username, host, port);
}
// generate a synthetic entry
entry = new HostConfigEntry(host, host, port, username);
} else {
if (log.isDebugEnabled()) {
log.debug("connect({}@{}:{}) effective: {}", username, host, port, entry);
}
}
return connect(entry);
}
@Override
public ConnectFuture connect(String username, SocketAddress address) throws IOException {
ValidateUtils.checkNotNull(address, "No target address");
if (address instanceof InetSocketAddress) {
InetSocketAddress inetAddress = (InetSocketAddress) address;
String host = ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host");
int port = inetAddress.getPort();
ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
HostConfigEntryResolver resolver = getHostConfigEntryResolver();
HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, username);
if (entry == null) {
if (log.isDebugEnabled()) {
log.debug("connect({}@{}:{}) no overrides", username, host, port);
}
return doConnect(username, address, Collections.<KeyPair>emptyList(), true);
} else {
if (log.isDebugEnabled()) {
log.debug("connect({}@{}:{}) effective: {}", username, host, port, entry);
}
return connect(entry);
}
} else {
if (log.isDebugEnabled()) {
log.debug("connect({}@{}) not an InetSocketAddress: {}", username, address, address.getClass().getName());
}
return doConnect(username, address, Collections.<KeyPair>emptyList(), true);
}
}
@Override
public ConnectFuture connect(HostConfigEntry hostConfig) throws IOException {
ValidateUtils.checkNotNull(hostConfig, "No host configuration");
String host = ValidateUtils.checkNotNullAndNotEmpty(hostConfig.getHostName(), "No target host");
int port = hostConfig.getPort();
ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
Collection<KeyPair> keys = loadClientIdentities(hostConfig.getIdentities(), IoUtils.EMPTY_LINK_OPTIONS);
return doConnect(hostConfig.getUsername(), new InetSocketAddress(host, port), keys, !hostConfig.isIdentitiesOnly());
}
protected List<KeyPair> loadClientIdentities(Collection<String> locations, LinkOption ... options) throws IOException {
if (GenericUtils.isEmpty(locations)) {
return Collections.emptyList();
}
List<KeyPair> ids = new ArrayList<>(locations.size());
boolean ignoreNonExisting = PropertyResolverUtils.getBooleanProperty(this, IGNORE_INVALID_IDENTITIES, DEFAULT_IGNORE_INVALID_IDENTITIES);
ClientIdentityLoader loader = ValidateUtils.checkNotNull(getClientIdentityLoader(), "No ClientIdentityLoader");
FilePasswordProvider provider = ValidateUtils.checkNotNull(getFilePasswordProvider(), "No FilePasswordProvider");
for (String l : locations) {
if (!loader.isValidLocation(l)) {
if (ignoreNonExisting) {
log.debug("loadClientIdentities - skip non-existing identity location: {}", l);
continue;
}
throw new FileNotFoundException("Invalid identity location: " + l);
}
try {
KeyPair kp = loader.loadClientIdentity(l, provider);
if (kp == null) {
throw new IOException("No identity loaded from " + l);
}
if (log.isDebugEnabled()) {
log.debug("loadClientIdentities({}) type={}, fingerprint={}",
l, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
}
ids.add(kp);
} catch (GeneralSecurityException e) {
throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ") to load identity from " + l + ": " + e.getMessage());
}
}
return ids;
}
protected ConnectFuture doConnect(
String username, SocketAddress address, Collection<? extends KeyPair> identities, boolean useDefaultIdentities)
throws IOException {
if (connector == null) {
throw new IllegalStateException("SshClient not started. Please call start() method before connecting to a server");
}
ConnectFuture connectFuture = new DefaultConnectFuture(null);
SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener(connectFuture, username, address, identities, useDefaultIdentities);
connector.connect(address).addListener(listener);
return connectFuture;
}
protected SshFutureListener<IoConnectFuture> createConnectCompletionListener(
final ConnectFuture connectFuture, final String username, final SocketAddress address,
final Collection<? extends KeyPair> identities, final boolean useDefaultIdentities) {
return new SshFutureListener<IoConnectFuture>() {
@Override
@SuppressWarnings("synthetic-access")
public void operationComplete(IoConnectFuture future) {
if (future.isCanceled()) {
connectFuture.cancel();
return;
}
Throwable t = future.getException();
if (t != null) {
if (log.isDebugEnabled()) {
log.debug("operationComplete({}@{}) failed ({}): {}",
username, address, t.getClass().getSimpleName(), t.getMessage());
}
connectFuture.setException(t);
} else {
onConnectOperationComplete(future.getSession(), connectFuture, username, address, identities, useDefaultIdentities);
}
}
};
}
protected void onConnectOperationComplete(IoSession ioSession, ConnectFuture connectFuture,
String username, SocketAddress address, Collection<? extends KeyPair> identities, boolean useDefaultIdentities) {
AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession);
session.setUsername(username);
session.setConnectAddress(address);
if (useDefaultIdentities) {
setupDefaultSessionIdentities(session);
}
int numIds = GenericUtils.size(identities);
if (numIds > 0) {
if (log.isDebugEnabled()) {
log.debug("onConnectOperationComplete({}) adding {} identities", session, numIds);
}
for (KeyPair kp : identities) {
if (log.isTraceEnabled()) {
log.trace("onConnectOperationComplete({}) add identity type={}, fingerprint={}",
session, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
}
session.addPublicKeyIdentity(kp);
}
}
connectFuture.setSession(session);
}
protected void setupDefaultSessionIdentities(ClientSession session) {
// check if session listener intervened
KeyPairProvider kpSession = session.getKeyPairProvider();
KeyPairProvider kpClient = getKeyPairProvider();
if (kpSession == null) {
session.setKeyPairProvider(kpClient);
} else {
if (kpSession != kpClient) {
if (log.isDebugEnabled()) {
log.debug("setupDefaultSessionIdentities({}) key-pair provider override", session);
}
}
}
PasswordIdentityProvider passSession = session.getPasswordIdentityProvider();
PasswordIdentityProvider passClient = getPasswordIdentityProvider();
if (passSession == null) {
session.setPasswordIdentityProvider(passClient);
} else {
if (passSession != passClient) {
if (log.isDebugEnabled()) {
log.debug("setupDefaultSessionIdentities({}) password provider override", session);
}
}
}
AuthenticationIdentitiesProvider idsClient = getRegisteredIdentities();
for (Iterator<?> iter = GenericUtils.iteratorOf((idsClient == null) ? null : idsClient.loadIdentities()); iter.hasNext();) {
Object id = iter.next();
if (id instanceof String) {
if (log.isTraceEnabled()) {
log.trace("setupDefaultSessionIdentities({}) add password fingerprint={}",
session, KeyUtils.getFingerPrint(id.toString()));
}
session.addPasswordIdentity((String) id);
} else if (id instanceof KeyPair) {
KeyPair kp = (KeyPair) id;
if (log.isTraceEnabled()) {
log.trace("setupDefaultSessionIdentities({}) add identity type={}, fingerprint={}",
session, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
}
session.addPublicKeyIdentity(kp);
} else {
if (log.isDebugEnabled()) {
log.debug("setupDefaultSessionIdentities({}) ignored identity={}", session, id);
}
}
}
}
protected IoConnector createConnector() {
return getIoServiceFactory().createConnector(getSessionFactory());
}
protected SessionFactory createSessionFactory() {
return new SessionFactory(this);
}
@Override
public String toString() {
return "SshClient[" + Integer.toHexString(hashCode()) + "]";
}
/**
* Setup a default client, starts it and then wraps it as a {@link SimpleClient}
*
* @return The {@link SimpleClient} wrapper. <B>Note:</B> when the wrapper
* is closed the client is also stopped
* @see #setUpDefaultClient()
* @see #wrapAsSimpleClient(SshClient)
*/
public static SimpleClient setUpDefaultSimpleClient() {
SshClient client = setUpDefaultClient();
client.start();
return wrapAsSimpleClient(client);
}
/**
* Wraps an {@link SshClient} instance as a {@link SimpleClient}
*
* @param client The client instance - never {@code null}. <B>Note:</B>
* client must be started <U>before</U> the simple client wrapper is used.
* @return The {@link SimpleClient} wrapper. <B>Note:</B> when the
* wrapper is closed the client is also stopped
*/
public static SimpleClient wrapAsSimpleClient(final SshClient client) {
ValidateUtils.checkNotNull(client, "No client instance");
// wrap the client so that close() is also stop()
final java.nio.channels.Channel channel = new java.nio.channels.Channel() {
@Override
public boolean isOpen() {
return client.isOpen();
}
@Override
public void close() throws IOException {
Exception err = null;
try {
client.close();
} catch (Exception e) {
err = GenericUtils.accumulateException(err, e);
}
try {
client.stop();
} catch (Exception e) {
err = GenericUtils.accumulateException(err, e);
}
if (err != null) {
if (err instanceof IOException) {
throw (IOException) err;
} else {
throw new IOException(err);
}
}
}
};
return AbstractSimpleClientSessionCreator.wrap(client, channel);
}
/**
* Setup a default client. The client does not require any additional setup.
*
* @return a newly create SSH client
*/
public static SshClient setUpDefaultClient() {
return ClientBuilder.builder().build();
}
/*=================================
Main class implementation
*=================================*/
public static boolean showError(PrintStream stderr, String message) {
stderr.println(message);
return true;
}
public static boolean isArgumentedOption(String portOption, String argName) {
return portOption.equals(argName)
|| "-i".equals(argName)
|| "-o".equals(argName)
|| "-l".equals(argName)
|| "-w".equals(argName)
|| "-c".equals(argName)
|| "-m".equals(argName)
|| "-E".equals(argName);
}
// NOTE: ClientSession#getFactoryManager is the SshClient
public static ClientSession setupClientSession(
String portOption, BufferedReader stdin, PrintStream stdout, PrintStream stderr, String... args)
throws Exception {
int port = -1;
String host = null;
String login = null;
String password = null;
boolean error = false;
List<File> identities = new ArrayList<>();
Map<String, String> options = new LinkedHashMap<>();
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(new File(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;
}
options.put(opt.substring(0, idx), opt.substring(idx + 1));
} 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;
}
}
// returns null if error encountered
public static SshClient setupClient(
Map<String, ?> options,
List<NamedFactory<Cipher>> ciphers,
List<NamedFactory<Mac>> macs,
List<NamedFactory<Compression>> compressions,
Collection<File> identities,
BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
if (GenericUtils.isEmpty(ciphers)) {
ciphers = setupCiphers(options, stderr);
if (ciphers == null) {
return null;
}
}
if (GenericUtils.isEmpty(macs)) {
macs = setupMacs(options, stderr);
if (macs == null) {
return null;
}
}
if (GenericUtils.isEmpty(compressions)) {
compressions = setupCompressions(options, 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 AbstractFileKeyPairProvider setupSessionIdentities(ClientFactoryManager client, Collection<File> identities,
final BufferedReader stdin, final PrintStream stdout, final PrintStream stderr)
throws Throwable {
client.setFilePasswordProvider(new FilePasswordProvider() {
@Override
public String getPassword(String file) throws IOException {
stdout.print("Enter password for private key file=" + file + ": ");
return stdin.readLine();
}
});
if (GenericUtils.isEmpty(identities)) {
return null;
}
AbstractFileKeyPairProvider provider = SecurityUtils.createFileKeyPairProvider();
provider.setFiles(identities);
client.setKeyPairProvider(provider);
return provider;
}
public static UserInteraction setupSessionUserInteraction(ClientAuthenticationManager client,
final BufferedReader stdin, final PrintStream stdout, final 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,
final BufferedReader stdin, final PrintStream stdout, final 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(new ModifiedServerKeyAcceptor() {
@Override
public boolean acceptModifiedServerKey(ClientSession clientSession, SocketAddress remoteAddress,
KnownHostEntry entry, PublicKey expected, PublicKey actual) throws Exception {
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(Map<String, ?> options, PrintStream stderr) {
String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.COMPRESSION_PROP);
if (GenericUtils.isEmpty(argVal)) {
return Collections.<NamedFactory<Compression>>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.Utils.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(Map<String, ?> options, PrintStream stderr) {
String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.MACS_CONFIG_PROP);
return GenericUtils.isEmpty(argVal)
? Collections.<NamedFactory<Mac>>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.Utils.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(Map<String, ?> options, PrintStream stderr) {
String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.CIPHERS_CONFIG_PROP);
return GenericUtils.isEmpty(argVal)
? Collections.<NamedFactory<Cipher>>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.Utils.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);
}
throwable = sw.toString();
}
return String.format("%1$tY-%1$tm-%1$td: %2$-7.7s: %3$-32.32s: %4$s%5$s%n",
new Date(record.getMillis()), record.getLevel().getName(),
record.getLoggerName(), message, throwable);
}
});
Logger root = Logger.getLogger("");
for (Handler handler : root.getHandlers()) {
root.removeHandler(handler);
}
root.addHandler(fh);
root.setLevel(level);
return fh;
}
//////////////////////////////////////////////////////////////////////////
public static void main(String[] args) throws Exception {
PrintStream stdout = System.out;
PrintStream stderr = System.err;
boolean agentForward = false;
List<String> command = null;
int socksPort = -1;
int numArgs = GenericUtils.length(args);
boolean error = false;
String target = null;
Level level = Level.WARNING;
OutputStream logStream = stderr;
for (int i = 0; i < numArgs; i++) {
String argName = args[i];
// handled by 'setupClientSession'
if (GenericUtils.isEmpty(command) && isArgumentedOption("-p", argName)) {
if ((i + 1) >= numArgs) {
error = showError(stderr, "option requires an argument: " + argName);
break;
}
i++;
continue;
}
// verbosity handled separately
if (GenericUtils.isEmpty(command) && ("-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName))) {
continue;
}
if (GenericUtils.isEmpty(command) && "-D".equals(argName)) {
if ((i + 1) >= numArgs) {
error = showError(stderr, "option requires an argument: " + argName);
break;
}
if (socksPort > 0) {
error = showError(stderr, argName + " option value re-specified: " + socksPort);
break;
}
socksPort = Integer.parseInt(args[++i]);
if (socksPort <= 0) {
error = showError(stderr, "Bad option value for " + argName + ": " + socksPort);
break;
}
} else if (GenericUtils.isEmpty(command) && "-A".equals(argName)) {
agentForward = true;
} else if (GenericUtils.isEmpty(command) && "-a".equals(argName)) {
agentForward = false;
} else {
level = resolveLoggingVerbosity(args, i);
logStream = resolveLoggingTargetStream(stdout, stderr, args, i);
if (logStream == null) {
error = true;
break;
}
if (GenericUtils.isEmpty(command) && target == null) {
target = argName;
} else {
if (command == null) {
command = new ArrayList<>();
}
command.add(argName);
}
}
}
ClientSession session = null;
try (BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
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 {
StringBuilder w = new StringBuilder(command.size() * Integer.SIZE);
for (String cmd : command) {
w.append(cmd).append(' ');
}
channel = session.createExecChannel(w.toString().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 != stdout) && (logStream != stderr)) {
logStream.close();
}
}
}
}