All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opendaylight.netconf.shaded.sshd.client.SshClient Maven / Gradle / Ivy

There is a newer version: 8.0.3
Show newest version
/*
 * 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.opendaylight.netconf.shaded.sshd.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.channels.UnsupportedAddressTypeException;
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.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.opendaylight.netconf.shaded.sshd.agent.SshAgentFactory;
import org.opendaylight.netconf.shaded.sshd.client.auth.AuthenticationIdentitiesProvider;
import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
import org.opendaylight.netconf.shaded.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.opendaylight.netconf.shaded.sshd.client.auth.keyboard.UserInteraction;
import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordAuthenticationReporter;
import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKey;
import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
import org.opendaylight.netconf.shaded.sshd.client.config.hosts.HostConfigEntry;
import org.opendaylight.netconf.shaded.sshd.client.config.hosts.HostConfigEntryResolver;
import org.opendaylight.netconf.shaded.sshd.client.config.keys.ClientIdentity;
import org.opendaylight.netconf.shaded.sshd.client.config.keys.ClientIdentityLoader;
import org.opendaylight.netconf.shaded.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture;
import org.opendaylight.netconf.shaded.sshd.client.future.ConnectFuture;
import org.opendaylight.netconf.shaded.sshd.client.future.DefaultConnectFuture;
import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
import org.opendaylight.netconf.shaded.sshd.client.session.AbstractClientSession;
import org.opendaylight.netconf.shaded.sshd.client.session.ClientConnectionServiceFactory;
import org.opendaylight.netconf.shaded.sshd.client.session.ClientProxyConnector;
import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
import org.opendaylight.netconf.shaded.sshd.client.session.ClientUserAuthServiceFactory;
import org.opendaylight.netconf.shaded.sshd.client.session.SessionFactory;
import org.opendaylight.netconf.shaded.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.opendaylight.netconf.shaded.sshd.client.simple.AbstractSimpleClientSessionCreator;
import org.opendaylight.netconf.shaded.sshd.client.simple.SimpleClient;
import org.opendaylight.netconf.shaded.sshd.common.AttributeRepository;
import org.opendaylight.netconf.shaded.sshd.common.Closeable;
import org.opendaylight.netconf.shaded.sshd.common.Factory;
import org.opendaylight.netconf.shaded.sshd.common.NamedResource;
import org.opendaylight.netconf.shaded.sshd.common.ServiceFactory;
import org.opendaylight.netconf.shaded.sshd.common.channel.ChannelFactory;
import org.opendaylight.netconf.shaded.sshd.common.config.keys.FilePasswordProvider;
import org.opendaylight.netconf.shaded.sshd.common.config.keys.FilePasswordProviderManager;
import org.opendaylight.netconf.shaded.sshd.common.config.keys.KeyUtils;
import org.opendaylight.netconf.shaded.sshd.common.config.keys.PublicKeyEntry;
import org.opendaylight.netconf.shaded.sshd.common.future.CancelFuture;
import org.opendaylight.netconf.shaded.sshd.common.future.Cancellable;
import org.opendaylight.netconf.shaded.sshd.common.future.SshFutureListener;
import org.opendaylight.netconf.shaded.sshd.common.helpers.AbstractFactoryManager;
import org.opendaylight.netconf.shaded.sshd.common.io.IoConnectFuture;
import org.opendaylight.netconf.shaded.sshd.common.io.IoConnector;
import org.opendaylight.netconf.shaded.sshd.common.io.IoSession;
import org.opendaylight.netconf.shaded.sshd.common.keyprovider.AbstractResourceKeyPairProvider;
import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
import org.opendaylight.netconf.shaded.sshd.common.keyprovider.MultiKeyIdentityProvider;
import org.opendaylight.netconf.shaded.sshd.common.session.helpers.AbstractSession;
import org.opendaylight.netconf.shaded.sshd.common.util.ExceptionUtils;
import org.opendaylight.netconf.shaded.sshd.common.util.GenericUtils;
import org.opendaylight.netconf.shaded.sshd.common.util.ValidateUtils;
import org.opendaylight.netconf.shaded.sshd.common.util.functors.UnaryEquator;
import org.opendaylight.netconf.shaded.sshd.common.util.io.resource.PathResource;
import org.opendaylight.netconf.shaded.sshd.common.util.net.SshdSocketAddress;
import org.opendaylight.netconf.shaded.sshd.core.CoreModuleProperties;

/**
 * 

* Entry point for the client side of the SSH protocol. *

* *

* The default configured client can be created using the {@link #setUpDefaultClient()}. The next step is to configure * and then start the client using the {@link #start()} method. *

* *

* Sessions can then be created using on of the {@link #connect(String, String, int)} or * {@link #connect(String, java.net.SocketAddress)} methods. *

* *

* The client can be stopped any time using the {@link #stop()} method. *

* *

* Following is an example of using the {@code SshClient}: *

* *
 * 
 * try (SshClient client = SshClient.setUpDefaultClient()) {
 *      ...further configuration of the client...
 *      client.start();
 *
 *      try (ClientSession session = client.connect(login, host, port)
 *                  .verify(...timeout...)
 *                  .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();
 *    }
 * }
 * 
 * 
* * Note: the idea is to have one {@code SshClient} instance for the entire application and re-use it * repeatedly in order to create as many sessions as necessary - possibly with different hosts, ports, users, passwords, * etc. - including concurrently. In other words, except for exceptional cases, it is recommended to initialize * one instance of {@code SshClient} for the application and then use throughout - including for multi-threading. As * long as the {@code SshClient} is not re-configured it should be multi-thread safe regardless of the target session * being created. * * @author Apache MINA SSHD Project */ public class SshClient extends AbstractFactoryManager implements ClientFactoryManager { public static final Factory DEFAULT_SSH_CLIENT_FACTORY = SshClient::new; /** * Default user authentication preferences if not set * * @see ssh_config(5) - PreferredAuthentications */ public static final List DEFAULT_USER_AUTH_FACTORIES = Collections.unmodifiableList( Arrays.asList( UserAuthPublicKeyFactory.INSTANCE, UserAuthKeyboardInteractiveFactory.INSTANCE, UserAuthPasswordFactory.INSTANCE)); public static final List DEFAULT_SERVICE_FACTORIES = Collections.unmodifiableList( Arrays.asList( ClientUserAuthServiceFactory.INSTANCE, ClientConnectionServiceFactory.INSTANCE)); protected IoConnector connector; protected SessionFactory sessionFactory; protected List userAuthFactories; private ClientProxyConnector proxyConnector; private ServerKeyVerifier serverKeyVerifier; private HostConfigEntryResolver hostConfigEntryResolver; private ClientIdentityLoader clientIdentityLoader; private KeyIdentityProvider keyIdentityProvider; private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter; private FilePasswordProvider filePasswordProvider; private PasswordIdentityProvider passwordIdentityProvider; private PasswordAuthenticationReporter passwordAuthenticationReporter; private HostBasedAuthenticationReporter hostBasedAuthenticationReporter; private UserInteraction userInteraction; private final List identities = new CopyOnWriteArrayList<>(); private final AuthenticationIdentitiesProvider identitiesProvider; private final AtomicBoolean started = new AtomicBoolean(false); public SshClient() { identitiesProvider = AuthenticationIdentitiesProvider.wrapIdentities(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 ServerKeyVerifier getServerKeyVerifier() { return serverKeyVerifier; } @Override public void setServerKeyVerifier(ServerKeyVerifier serverKeyVerifier) { this.serverKeyVerifier = Objects.requireNonNull(serverKeyVerifier, "No server key verifier"); } @Override public HostConfigEntryResolver getHostConfigEntryResolver() { return hostConfigEntryResolver; } @Override public void setHostConfigEntryResolver(HostConfigEntryResolver resolver) { this.hostConfigEntryResolver = Objects.requireNonNull(resolver, "No host configuration entry resolver"); } @Override public FilePasswordProvider getFilePasswordProvider() { return filePasswordProvider; } @Override public void setFilePasswordProvider(FilePasswordProvider provider) { this.filePasswordProvider = Objects.requireNonNull(provider, "No file password provider"); } @Override public ClientIdentityLoader getClientIdentityLoader() { return clientIdentityLoader; } @Override public void setClientIdentityLoader(ClientIdentityLoader loader) { this.clientIdentityLoader = Objects.requireNonNull(loader, "No client identity loader"); } @Override public UserInteraction getUserInteraction() { return userInteraction; } @Override public void setUserInteraction(UserInteraction userInteraction) { this.userInteraction = userInteraction; } @Override public PasswordAuthenticationReporter getPasswordAuthenticationReporter() { return passwordAuthenticationReporter; } @Override public void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter) { this.passwordAuthenticationReporter = reporter; } @Override public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() { return hostBasedAuthenticationReporter; } @Override public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) { this.hostBasedAuthenticationReporter = reporter; } @Override public List getUserAuthFactories() { return userAuthFactories; } @Override public void setUserAuthFactories(List 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) { // DO NOT USE checkNotNullOrNotEmpty SINCE IT TRIMS THE RESULT ValidateUtils.checkTrue((password != null) && (!password.isEmpty()), "No password provided"); identities.add(password); 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.findIdentityIndex( identities, AuthenticationIdentitiesProvider.PASSWORD_IDENTITY_COMPARATOR, password); if (index >= 0) { return (String) identities.remove(index); } else { return null; } } @Override public void addPublicKeyIdentity(KeyPair kp) { Objects.requireNonNull(kp, "No key-pair to add"); Objects.requireNonNull(kp.getPublic(), "No public key"); Objects.requireNonNull(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.findIdentityIndex( identities, AuthenticationIdentitiesProvider.KEYPAIR_IDENTITY_COMPARATOR, kp); if (index >= 0) { return (KeyPair) identities.remove(index); } else { return null; } } @Override public KeyIdentityProvider getKeyIdentityProvider() { return keyIdentityProvider; } @Override public void setKeyIdentityProvider(KeyIdentityProvider keyIdentityProvider) { this.keyIdentityProvider = keyIdentityProvider; } @Override public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() { return publicKeyAuthenticationReporter; } @Override public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) { this.publicKeyAuthenticationReporter = reporter; } @Override protected void checkConfig() { super.checkConfig(); Objects.requireNonNull(getForwarderFactory(), "ForwarderFactory not set"); Objects.requireNonNull(getServerKeyVerifier(), "ServerKeyVerifier not set"); Objects.requireNonNull(getHostConfigEntryResolver(), "HostConfigEntryResolver not set"); Objects.requireNonNull(getClientIdentityLoader(), "ClientIdentityLoader not set"); Objects.requireNonNull(getFilePasswordProvider(), "FilePasswordProvider not set"); // if no client identities override use the default KeyIdentityProvider defaultIdentities = getKeyIdentityProvider(); if (defaultIdentities == null) { KeyIdentityProvider idsWatcher = new DefaultClientIdentitiesWatcher( this::getClientIdentityLoader, this::getFilePasswordProvider); setKeyIdentityProvider(idsWatcher); } // Register the additional agent forwarding channel(s) if needed SshAgentFactory agentFactory = getAgentFactory(); if (agentFactory != null) { List forwarders = agentFactory.getChannelForwardingFactories(this); if (!GenericUtils.isEmpty(forwarders)) { List factories = getChannelFactories(); if (GenericUtils.isEmpty(factories)) { factories = forwarders; } else { // create a copy in case un-modifiable original List factories2 = new ArrayList<>(factories.size() + forwarders.size()); factories2.addAll(factories); factories2.addAll(forwarders); factories = factories2; } setChannelFactories(factories); } } if (GenericUtils.isEmpty(getServiceFactories())) { setServiceFactories(DEFAULT_SERVICE_FACTORIES); } if (GenericUtils.isEmpty(getUserAuthFactories())) { setUserAuthFactories(DEFAULT_USER_AUTH_FACTORIES); } } public boolean isStarted() { return started.get(); } /** * Starts the SSH client and can start creating sessions using it. Ignored if already {@link #isStarted() started}. */ public void start() { if (isClosed()) { throw new IllegalStateException("Can not start the client again"); } if (isStarted()) { return; } checkConfig(); if (sessionFactory == null) { sessionFactory = createSessionFactory(); } setupSessionTimeout(sessionFactory); connector = createConnector(); started.set(true); } public void stop() { if (!started.getAndSet(false)) { return; } try { Duration maxWait = CoreModuleProperties.STOP_WAIT_TIME.getRequired(this); boolean successful = close(true).await(maxWait); if (!successful) { throw new SocketTimeoutException("Failed to receive closure confirmation within " + maxWait + " millis"); } } catch (IOException e) { warn("{} while stopping client: {}", e.getClass().getSimpleName(), e.getMessage(), e); } finally { // clear the attributes since we close stop the client clearAttributes(); } } public void open() throws IOException { start(); } @Override protected Closeable getInnerCloseable() { Object closeId = toString(); return builder() .run(closeId, () -> removeSessionTimeout(sessionFactory)) .sequential(connector, ioServiceFactory) .run(closeId, () -> { connector = null; ioServiceFactory = null; if (shutdownExecutor && (executor != null) && (!executor.isShutdown())) { try { executor.shutdownNow(); } finally { executor = null; } } }) .build(); } @Override public ConnectFuture connect(String uriStr) throws IOException { Objects.requireNonNull(uriStr, "No uri address"); URI uri = URI.create(uriStr.contains("//") ? uriStr : "ssh://" + uriStr); if (GenericUtils.isNotEmpty(uri.getScheme()) && !"ssh".equals(uri.getScheme())) { throw new IllegalArgumentException("Unsupported scheme for uri: " + uri); } String host = uri.getHost(); int port = uri.getPort(); String userInfo = uri.getUserInfo(); return connect(userInfo, host, port); } @Override public ConnectFuture connect( String username, SocketAddress targetAddress, AttributeRepository context, SocketAddress localAddress) throws IOException { Objects.requireNonNull(targetAddress, "No target address"); if (!(targetAddress instanceof InetSocketAddress)) { throw new UnsupportedAddressTypeException(); } InetSocketAddress inetAddress = (InetSocketAddress) targetAddress; String host = ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host"); int port = inetAddress.getPort(); ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port); return connect(username, host, port, context, localAddress); } @Override public ConnectFuture connect( String username, String host, int port, AttributeRepository context, SocketAddress localAddress) throws IOException { HostConfigEntry entry = resolveHost(username, host, port, context, localAddress); return connect(entry, context, localAddress); } @Override public ConnectFuture connect( HostConfigEntry hostConfig, AttributeRepository context, SocketAddress localAddress) throws IOException { List jumps = parseProxyJumps(hostConfig, context); return doConnect(hostConfig, jumps, context, localAddress); } protected ConnectFuture doConnect( HostConfigEntry hostConfig, List jumps, AttributeRepository context, SocketAddress localAddress) throws IOException { Objects.requireNonNull(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 hostIds = hostConfig.getIdentities(); Collection idFiles = GenericUtils.stream(hostIds) .map(Paths::get) .map(PathResource::new) .collect(Collectors.toCollection(() -> new ArrayList<>(hostIds.size()))); KeyIdentityProvider keys = preloadClientIdentities(idFiles); String username = hostConfig.getUsername(); InetSocketAddress targetAddress = new InetSocketAddress(hostConfig.getHostName(), hostConfig.getPort()); if (GenericUtils.isNotEmpty(jumps)) { ConnectFuture connectFuture = new DefaultConnectFuture(username + "@" + targetAddress, null); HostConfigEntry jump = jumps.remove(0); ConnectFuture f1 = doConnect(jump, jumps, context, null); AtomicReference toCancel = new AtomicReference<>(f1); connectFuture.addListener(c -> { if (!c.isCanceled()) { return; } Cancellable inner = toCancel.get(); if (inner == null) { return; } CancelFuture cancellation = inner.cancel(); if (cancellation == null) { return; } cancellation.addListener(cf -> { if (cf.isDone()) { c.getCancellation().setCanceled(); } }); }); f1.addListener(f2 -> { if (f2.isConnected()) { ClientSession proxySession = f2.getClientSession(); try { if (connectFuture.isCanceled()) { proxySession.close(true); } AuthFuture auth = proxySession.auth(); toCancel.set(auth); auth.addListener(f3 -> { if (f3.isSuccess()) { try { SshdSocketAddress address = new SshdSocketAddress(hostConfig.getHostName(), hostConfig.getPort()); ExplicitPortForwardingTracker tracker = proxySession .createLocalPortForwardingTracker(SshdSocketAddress.LOCALHOST_ADDRESS, address); SshdSocketAddress bound = tracker.getBoundAddress(); ConnectFuture f4 = doConnect(hostConfig.getUsername(), bound.toInetSocketAddress(), context, localAddress, keys, hostConfig); toCancel.set(f4); if (connectFuture.isCanceled()) { f4.cancel(); } f4.addListener(f5 -> { if (f5.isConnected()) { ClientSession clientSession = f5.getClientSession(); clientSession.setAttribute(TARGET_SERVER, address); connectFuture.setSession(clientSession); proxySession.addCloseFutureListener(f6 -> clientSession.close(true)); clientSession.addCloseFutureListener(f6 -> proxySession.close(true)); } else { proxySession.close(true); connectFuture.setException(f5.getException()); } }); } catch (IOException e) { proxySession.close(true); connectFuture.setException(e); } } else { proxySession.close(true); connectFuture.setException(f3.getException()); } }); } catch (IOException e) { proxySession.close(true); connectFuture.setException(e); } } else { connectFuture.setException(f2.getException()); } }); return connectFuture; } else { return doConnect(hostConfig.getUsername(), new InetSocketAddress(host, port), context, localAddress, keys, hostConfig); } } protected ConnectFuture doConnect( String username, SocketAddress targetAddress, AttributeRepository context, SocketAddress localAddress, KeyIdentityProvider identities, HostConfigEntry hostConfig) throws IOException { if (connector == null) { throw new IllegalStateException("SshClient not started. Please call start() method before connecting to a server"); } ConnectFuture connectFuture = new DefaultConnectFuture(username + "@" + targetAddress, null); SshFutureListener listener = createConnectCompletionListener( connectFuture, username, targetAddress, identities, hostConfig); IoConnectFuture connectingFuture = connector.connect(targetAddress, context, localAddress); connectFuture.addListener(c -> { if (c.isCanceled()) { connectingFuture.cancel(); } }); connectingFuture.addListener(listener); return connectFuture; } protected List parseProxyJumps(HostConfigEntry entry, AttributeRepository context) throws IOException { List hops; try { hops = parseProxyJumps(entry.getProxyJump(), context); if (GenericUtils.isEmpty(hops)) { return hops; } // If the last entry itself has proxy jumps, we need to append them. Guard against possible proxy jump // loops by imposing an upper limit on the total number of jumps. for (;;) { HostConfigEntry last = hops.get(hops.size() - 1); try { List additionalHops = parseProxyJumps(last.getProxyJump(), context); if (additionalHops.isEmpty()) { break; } else { hops.addAll(additionalHops); if (hops.size() > CoreModuleProperties.MAX_PROXY_JUMPS.getRequired(this)) { throw new IllegalArgumentException("Too many proxy jumps for host " + entry.getHost()); } } } catch (IOException | RuntimeException e) { throw new IllegalArgumentException("Problem parsing proxyJump from host config " + last.getHost(), e); } } } catch (IOException | RuntimeException e) { throw new IllegalArgumentException("Problem parsing proxyJump from host config " + entry.getHost(), e); } return hops; } protected List parseProxyJumps(String proxyJump, AttributeRepository context) throws IOException { List jumps = new ArrayList<>(); if (GenericUtils.isEmpty(proxyJump)) { return jumps; } String[] hops = GenericUtils.split(proxyJump, ','); for (String hop : hops) { String h = hop.trim(); if (h.isEmpty()) { throw new IllegalArgumentException("Empty proxy jump in list: " + proxyJump); } URI uri = URI.create(h.contains("://") ? h : "ssh://" + h); if (GenericUtils.isNotEmpty(uri.getScheme()) && !"ssh".equals(uri.getScheme())) { throw new IllegalArgumentException("Unsupported scheme for proxy jump: " + hop); } String host = uri.getHost(); int port = uri.getPort(); String userInfo = uri.getUserInfo(); HostConfigEntry entry = resolveHost(userInfo, host, port, context, null); jumps.add(entry); } return jumps; } protected HostConfigEntry resolveHost( String username, String host, int port, AttributeRepository context, SocketAddress localAddress) throws IOException { HostConfigEntryResolver resolver = getHostConfigEntryResolver(); HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, localAddress, username, null, context); if (entry == null) { // generate a synthetic entry if (log.isDebugEnabled()) { log.debug("connect({}@{}:{}) no overrides", username, host, port); } // IPv6 addresses have a format which means they need special treatment, separate from pattern validation if (SshdSocketAddress.isIPv6Address(host)) { // Not using a pattern as the host name passed in was a valid IPv6 address entry = new HostConfigEntry("", host, port, username, null); } else { entry = new HostConfigEntry(host, host, port, username, null); } } else { if (log.isDebugEnabled()) { log.debug("connect({}@{}:{}) effective: {}", username, host, port, entry); } } return entry; } protected KeyIdentityProvider preloadClientIdentities( Collection locations) throws IOException { return GenericUtils.isEmpty(locations) ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER : ClientIdentityLoader.asKeyIdentityProvider( Objects.requireNonNull(getClientIdentityLoader(), "No ClientIdentityLoader"), locations, getFilePasswordProvider(), CoreModuleProperties.IGNORE_INVALID_IDENTITIES.getRequired(this)); } protected SshFutureListener createConnectCompletionListener( ConnectFuture connectFuture, String username, SocketAddress address, KeyIdentityProvider identities, HostConfigEntry hostConfig) { return new SshFutureListener() { @Override @SuppressWarnings("synthetic-access") public void operationComplete(IoConnectFuture future) { if (future.isCanceled()) { CancelFuture cancellation = connectFuture.cancel(); if (cancellation != null) { future.getCancellation().addListener(f -> cancellation.setCanceled(f.getBackTrace())); } 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 { IoSession ioSession = future.getSession(); try { onConnectOperationComplete(ioSession, connectFuture, username, address, identities, hostConfig); } catch (IOException | GeneralSecurityException | RuntimeException e) { warn("operationComplete({}@{}) failed ({}) to signal completion of session={}: {}", username, address, e.getClass().getSimpleName(), ioSession, e.getMessage(), e); connectFuture.setException(e); ioSession.close(true); } } } @Override public String toString() { return "ConnectCompletionListener[" + username + "@" + address + "]"; } }; } protected void onConnectOperationComplete( IoSession ioSession, ConnectFuture connectFuture, String username, SocketAddress address, KeyIdentityProvider identities, HostConfigEntry hostConfig) throws IOException, GeneralSecurityException { AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession); session.setUsername(username); session.setConnectAddress(address); boolean useDefaultIdentities = !hostConfig.isIdentitiesOnly(); session.setAttribute(UserAuthPublicKey.USE_DEFAULT_IDENTITIES, Boolean.valueOf(useDefaultIdentities)); String identityAgent = hostConfig.getProperty(HostConfigEntry.IDENTITY_AGENT); session.setAttribute(UserAuthPublicKey.IDENTITY_AGENT, identityAgent == null ? "" : identityAgent); if (useDefaultIdentities) { setupDefaultSessionIdentities(session, identities); } else if (identities == null) { session.setKeyIdentityProvider(KeyIdentityProvider.EMPTY_KEYS_PROVIDER); } else { session.setKeyIdentityProvider(ensureFilePasswordProvider(identities)); } connectFuture.setSession(session); if (session != connectFuture.getSession()) { // Must have been canceled before try { session.close(true); } finally { CancelFuture cancellation = connectFuture.cancel(); if (cancellation != null) { cancellation.setCanceled(); } } } } /** * Sets this client's {@link FilePasswordProvider} on the {@link KeyIdentityProvider} if it is an * {@link AbstractResourceKeyPairProvider} or implements {@link FilePasswordProviderManager} and doesn't have one * yet. If the given {@code identities} is a {@link MultiKeyIdentityProvider}, the wrapped identity providers are * set up. * * @param identities {@link KeyIdentityProvider} to set up * @return a {@link KeyIdentityProvider} configured with a {@link FilePasswordProvider} * @see #getFilePasswordProvider() */ protected KeyIdentityProvider ensureFilePasswordProvider(KeyIdentityProvider identities) { if (identities instanceof AbstractResourceKeyPairProvider) { AbstractResourceKeyPairProvider keyProvider = (AbstractResourceKeyPairProvider) identities; if (keyProvider.getPasswordFinder() == null) { FilePasswordProvider passwordProvider = getFilePasswordProvider(); if (passwordProvider != null) { keyProvider.setPasswordFinder(passwordProvider); } } } else if (identities instanceof FilePasswordProviderManager) { FilePasswordProviderManager keyProvider = (FilePasswordProviderManager) identities; if (keyProvider.getFilePasswordProvider() == null) { FilePasswordProvider passwordProvider = getFilePasswordProvider(); if (passwordProvider != null) { keyProvider.setFilePasswordProvider(passwordProvider); } } } else if (identities instanceof MultiKeyIdentityProvider) { MultiKeyIdentityProvider multiProvider = (MultiKeyIdentityProvider) identities; multiProvider.getProviders().forEach(this::ensureFilePasswordProvider); } return identities; } protected void setupDefaultSessionIdentities( ClientSession session, KeyIdentityProvider extraIdentities) throws IOException, GeneralSecurityException { boolean debugEnabled = log.isDebugEnabled(); // check if session listener intervened KeyIdentityProvider kpSession = session.getKeyIdentityProvider(); KeyIdentityProvider kpClient = getKeyIdentityProvider(); if (UnaryEquator.isSameReference(kpSession, kpClient)) { if (debugEnabled) { log.debug("setupDefaultSessionIdentities({}) key identity provider override in session listener", session); } } // Prefer the extra identities to come first since they were probably indicated by the host-config entry KeyIdentityProvider kpEffective = KeyIdentityProvider.resolveKeyIdentityProvider(extraIdentities, kpSession); kpEffective = ensureFilePasswordProvider(kpEffective); if (!UnaryEquator.isSameReference(kpSession, kpEffective)) { if (debugEnabled) { log.debug("setupDefaultSessionIdentities({}) key identity provider enhanced", session); } session.setKeyIdentityProvider(kpEffective); } PasswordIdentityProvider passSession = session.getPasswordIdentityProvider(); PasswordIdentityProvider passClient = getPasswordIdentityProvider(); if (!UnaryEquator.isSameReference(passSession, passClient)) { if (debugEnabled) { log.debug("setupDefaultSessionIdentities({}) password provider override", session); } } AuthenticationIdentitiesProvider idsClient = getRegisteredIdentities(); boolean traceEnabled = log.isTraceEnabled(); for (Iterator iter = GenericUtils.iteratorOf((idsClient == null) ? null : idsClient.loadIdentities(session)); iter.hasNext();) { Object id = iter.next(); if (id instanceof String) { if (traceEnabled) { 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 (traceEnabled) { log.trace("setupDefaultSessionIdentities({}) add identity type={}, fingerprint={}", session, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic())); } session.addPublicKeyIdentity(kp); } else { if (debugEnabled) { 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 getClass().getSimpleName() + "[" + Integer.toHexString(hashCode()) + "]"; } /** * Setup a default client, starts it and then wraps it as a {@link SimpleClient} * * @return The {@link SimpleClient} wrapper. Note: 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}. Note: client must be started before the * simple client wrapper is used. * @return The {@link SimpleClient} wrapper. Note: when the wrapper is closed the client is also * stopped */ public static SimpleClient wrapAsSimpleClient(SshClient client) { Objects.requireNonNull(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 = ExceptionUtils.accumulateException(err, e); } try { client.stop(); } catch (Exception e) { err = ExceptionUtils.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 {@link SshClient} with default configurations */ public static SshClient setUpDefaultClient() { ClientBuilder builder = ClientBuilder.builder(); return builder.build(); } /** * @param The generic client class * @param client The {@link SshClient} to updated * @param strict If {@code true} then files that do not have the required access rights are * excluded from consideration * @param supportedOnly If {@code true} then ignore identities that are not supported internally * @param provider A {@link FilePasswordProvider} - may be {@code null} if the loaded keys are * guaranteed not to be encrypted. The argument to * {@code FilePasswordProvider#getPassword} is the path of the file whose key is to * be loaded * @param options The {@link LinkOption}s to apply when checking for existence * @return The updated client instance - provided a non-{@code null} * {@link KeyPairProvider} was generated * @throws IOException If failed to access the file system * @throws GeneralSecurityException If failed to load the keys * @see #setKeyPairProvider(SshClient, Path, boolean, boolean, FilePasswordProvider, * LinkOption...) */ public static C setKeyPairProvider( C client, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options) throws IOException, GeneralSecurityException { return setKeyPairProvider( client, PublicKeyEntry.getDefaultKeysFolderPath(), strict, supportedOnly, provider, options); } /** * @param The generic client class * @param client The {@link SshClient} to updated * @param dir The folder to scan for the built-in identities * @param strict If {@code true} then files that do not have the required access rights are * excluded from consideration * @param supportedOnly If {@code true} then ignore identities that are not supported internally * @param provider A {@link FilePasswordProvider} - may be {@code null} if the loaded keys are * guaranteed not to be encrypted. The argument to * {@code FilePasswordProvider#getPassword} is the path of the file whose key is to * be loaded * @param options The {@link LinkOption}s to apply when checking for existence * @return The updated client instance - provided a non-{@code null} * {@link KeyIdentityProvider} was generated * @throws IOException If failed to access the file system * @throws GeneralSecurityException If failed to load the keys * @see ClientIdentity#loadDefaultKeyPairProvider(Path, boolean, boolean, * FilePasswordProvider, LinkOption...) */ public static C setKeyPairProvider( C client, Path dir, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options) throws IOException, GeneralSecurityException { KeyIdentityProvider kpp = ClientIdentity.loadDefaultKeyPairProvider(dir, strict, supportedOnly, provider, options); if (kpp != null) { client.setKeyIdentityProvider(kpp); } return client; } }