blob: d39b1754072e1f50cee7c5c344c3f90fc9b65ede [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.sftp.spring.integration;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collection;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.simple.SimpleClientConfigurator;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.auth.MutableBasicCredentials;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.FilePasswordProviderManager;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.loader.pem.PEMResourceParserUtils;
import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.util.ExceptionUtils;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClient.DirEntry;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.client.SftpVersionSelector;
import org.apache.sshd.sftp.server.SftpSubsystemEnvironment;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.remote.session.SharedSessionCapable;
/**
* A proper replacement for the {@code org.springframework.integration.sftp.session.DefaultSftpSessionFactory}
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class ApacheSshdSftpSessionFactory
extends AbstractLoggingBean
implements SessionFactory<DirEntry>, SharedSessionCapable,
MutableBasicCredentials, FilePasswordProviderManager, SimpleClientConfigurator,
InitializingBean, DisposableBean {
private final boolean sharedSession;
private final AtomicReference<ClientSession> sharedSessionHolder = new AtomicReference<>();
private volatile String hostValue;
private volatile int portValue = SshConstants.DEFAULT_PORT;
private volatile String userValue;
private volatile String passwordValue;
// TODO add support for loading multiple private keys
private volatile KeyPair privateKeyPair;
private volatile Resource privateKey;
private volatile String privateKeyPassphrase;
private volatile FilePasswordProvider passwordProvider;
private volatile Properties sessionConfig;
private volatile long connTimeout = DEFAULT_CONNECT_TIMEOUT;
private volatile long authTimeout = DEFAULT_AUTHENTICATION_TIMEOUT;
private volatile SftpVersionSelector versionSelector = SftpVersionSelector.CURRENT;
private SshClient sshClient;
public ApacheSshdSftpSessionFactory() {
this(false);
}
public ApacheSshdSftpSessionFactory(boolean sharedSession) {
this.sharedSession = sharedSession;
}
public String getHost() {
return hostValue;
}
/**
* @param host The host to connect to - this is a mandatory property.
*/
public void setHost(String host) {
this.hostValue = ValidateUtils.checkNotNullAndNotEmpty(host, "No host name provided");
}
public int getPort() {
return portValue;
}
/**
* The port over which the SFTP connection shall be established. If not specified, this value defaults to
* <code>22</code>. If specified, this property must be a positive number.
*
* @param port The port value
*/
public void setPort(int port) {
ValidateUtils.checkTrue(port > 0, "Non-positive port value specified: %d", port);
this.portValue = port;
}
@Override
public String getUsername() {
return userValue;
}
/**
* The remote user to use. This is a mandatory property.
*
* @param user The (never {@code null}/empty) username
*/
@Override
public void setUsername(String user) {
this.userValue = ValidateUtils.checkNotNullAndNotEmpty(user, "No user specified: %s", user);
}
@Override
public String getPassword() {
return passwordValue;
}
/**
* The password to authenticate against the remote host. If a password is not provided, then a
* {@link #setPrivateKeyLocation(Resource)} call is mandatory.
*
* @param password The password to use - if {@code null} then no password is set - in which case the
* {@link #getPrivateKeyLocation()} resource is used
*/
@Override
public void setPassword(String password) {
this.passwordValue = password;
}
public Resource getPrivateKeyLocation() {
return privateKey;
}
/**
* Allows you to set a {@link Resource}, which represents the location of the private key used for authenticating
* against the remote host. If the privateKey is not provided, then the {@link #setPassword(String)} call is
* mandatory
*
* @param privateKey The private key {@link Resource}
*/
public void setPrivateKeyLocation(Resource privateKey) {
this.privateKey = privateKey;
}
public String getPrivateKeyPassphrase() {
return privateKeyPassphrase;
}
/**
* @param privateKeyPassphrase The password for the private key - required if the private key resource is encrypted
*/
public void setPrivateKeyPassphrase(String privateKeyPassphrase) {
this.privateKeyPassphrase = privateKeyPassphrase;
}
@Override
public FilePasswordProvider getFilePasswordProvider() {
return passwordProvider;
}
@Override
public void setFilePasswordProvider(FilePasswordProvider provider) {
this.passwordProvider = provider;
}
public KeyPair getPrivateKeyPair() {
return privateKeyPair;
}
public void setPrivateKeyPair(KeyPair privateKeyPair) {
this.privateKeyPair = privateKeyPair;
}
@Override // In seconds
public long getConnectTimeout() {
return connTimeout;
}
@Override
public void setConnectTimeout(long timeout) {
connTimeout = timeout;
}
@Override // In seconds
public long getAuthenticationTimeout() {
return authTimeout;
}
@Override
public void setAuthenticationTimeout(long timeout) {
authTimeout = timeout;
}
public Properties getSessionConfig() {
return sessionConfig;
}
/**
* @param sessionConfig Extra {@link Properties} that can be used to set specific SSHD session properties
*/
public void setSessionConfig(Properties sessionConfig) {
this.sessionConfig = sessionConfig;
}
public SshClient getSshClient() {
return sshClient;
}
public void setSshClient(SshClient sshClient) {
this.sshClient = sshClient;
}
@Override
public boolean isSharedSession() {
return sharedSession;
}
public SftpVersionSelector getSftpVersionSelector() {
return versionSelector;
}
public void setSftpVersion(String version) {
if ("CURRENT".equalsIgnoreCase(version)) {
setSftpVersionSelector(SftpVersionSelector.CURRENT);
} else if ("MAXIMUM".equalsIgnoreCase(version)) {
setSftpVersionSelector(SftpVersionSelector.MAXIMUM);
} else if ("MINIMUM".equalsIgnoreCase(version)) {
setSftpVersionSelector(SftpVersionSelector.MINIMUM);
} else {
int fixedVersion = Integer.parseInt(version);
ValidateUtils.checkTrue((fixedVersion >= SftpSubsystemEnvironment.LOWER_SFTP_IMPL)
&& (fixedVersion <= SftpSubsystemEnvironment.HIGHER_SFTP_IMPL),
"Unsupported SFTP version: %s", version);
setSftpVersionSelector(SftpVersionSelector.fixedVersionSelector(fixedVersion));
}
}
public void setSftpVersionSelector(SftpVersionSelector selector) {
versionSelector = Objects.requireNonNull(selector, "No version selector provided");
}
protected ClientSession getSharedClientSession() {
synchronized (sharedSessionHolder) {
return sharedSessionHolder.get();
}
}
@Override
public void resetSharedSession() {
ClientSession sharedSession;
synchronized (sharedSessionHolder) {
sharedSession = sharedSessionHolder.getAndSet(null);
}
if (sharedSession != null) {
log.info("resetSharedSession - session={}", sharedSession);
sharedSession.close(false).addListener(new SshFutureListener<CloseFuture>() {
@Override
@SuppressWarnings("synthetic-access")
public void operationComplete(CloseFuture future) {
log.info("resetSharedSession - session closed: {}", sharedSession);
}
});
}
}
@Override
public void afterPropertiesSet() throws Exception {
KeyPair kp = getPrivateKeyPair();
Resource privateKeyLocation = getPrivateKeyLocation();
ValidateUtils.checkState(
GenericUtils.isNotEmpty(getPassword()) || (kp != null) || (privateKeyLocation != null),
"Either password or private key must be provided");
SshClient client = getSshClient();
if (client == null) {
client = createSshClientInstance();
setSshClient(client);
}
if (!client.isOpen()) {
log.info("afterPropertiesSet() - starting client");
client.start();
log.info("afterPropertiesSet() - client started");
}
}
protected SshClient createSshClientInstance() throws Exception {
return SshClient.setUpDefaultClient();
}
@Override
public void destroy() throws Exception {
SshClient client = getSshClient();
if ((client != null) && client.isOpen()) {
log.info("destroy() - stopping client");
client.close(false); // do not wait for the close to complete
log.info("destroy() - client stopped");
}
}
protected KeyPair resolveKeyIdentity(ClientSession session) throws IOException, GeneralSecurityException {
KeyPair kp = getPrivateKeyPair();
if (kp != null) {
return kp;
}
Resource location = getPrivateKeyLocation();
if (location == null) {
return null;
}
kp = loadPrivateKey(session, location, getPrivateKeyPassphrase());
if (kp != null) {
setPrivateKeyPair(kp); // cache it for re-use
}
return kp;
}
protected FilePasswordProvider resolveFilePasswordProvider(
ClientSession session, Resource keyResource, String keyPassword) {
FilePasswordProvider provider = getFilePasswordProvider();
if (provider != null) {
return provider;
}
return GenericUtils.isEmpty(keyPassword)
? FilePasswordProvider.EMPTY
: FilePasswordProvider.of(keyPassword);
}
protected KeyPair loadPrivateKey(ClientSession session, Resource keyResource, String keyPassword)
throws IOException, GeneralSecurityException {
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
log.debug("loadPrivateKey({}) loading from {}", session, keyResource);
}
FilePasswordProvider provider = resolveFilePasswordProvider(session, keyResource, keyPassword);
SpringIoResource location = new SpringIoResource(keyResource);
Collection<KeyPair> keyPairs = PEMResourceParserUtils.PROXY.loadKeyPairs(session, location, provider);
int numLoaded = GenericUtils.size(keyPairs);
if (numLoaded <= 0) {
if (debugEnabled) {
log.debug("loadPrivateKey({}) no keys loaded from {}", session, keyResource);
}
}
// TODO add support for multiple keys
ValidateUtils.checkState(numLoaded == 1, "Multiple keys loaded from %s", keyResource);
KeyPair kp = GenericUtils.head(keyPairs);
if (debugEnabled) {
PublicKey pubKey = kp.getPublic();
log.debug("loadPrivateKey({}) loaded {} key={} from {}",
session, KeyUtils.getKeyType(pubKey), KeyUtils.getFingerPrint(pubKey), keyResource);
}
return kp;
}
@Override
public Session<DirEntry> getSession() {
boolean sharedInstance = isSharedSession();
try {
ClientSession session = null;
try {
session = resolveClientSession(sharedInstance);
SftpVersionSelector selector = getSftpVersionSelector();
SftpClientFactory sftpFactory = SftpClientFactory.instance();
SftpClient sftpClient = sftpFactory.createSftpClient(session, selector);
ClientSession sessionInstance = session;
Session<DirEntry> result = sharedInstance
? new SpringSftpSession(sftpClient)
: new SpringSftpSession(sftpClient, () -> {
try {
sessionInstance.close();
return null;
} catch (Exception e) {
return e;
}
});
// avoid auto-close at finally clause
session = null;
return result;
} finally {
if (session != null) {
try {
session.close();
} finally {
if (sharedInstance) {
resetSharedSession();
}
}
}
}
} catch (Exception e) {
throw ExceptionUtils.toRuntimeException(e);
}
}
protected ClientSession resolveClientSession(boolean sharedInstance) throws Exception {
ClientSession session;
if (sharedInstance) {
synchronized (sharedSessionHolder) {
session = sharedSessionHolder.get();
if (session == null) {
session = createClientSession();
}
sharedSessionHolder.set(session);
}
} else {
session = createClientSession();
}
return session;
}
protected ClientSession createClientSession() throws Exception {
String hostname = ValidateUtils.checkNotNullAndNotEmpty(getHost(), "Host must not be empty");
String username = ValidateUtils.checkNotNullAndNotEmpty(getUsername(), "User must not be empty");
ClientSession session
= createClientSession(hostname, username, getPort(), getEffectiveTimeoutValue(getConnectTimeout()));
try {
session = configureClientSessionProperties(session, getSessionConfig());
session = authenticateClientSession(session, getEffectiveTimeoutValue(getAuthenticationTimeout()));
ClientSession newSession = session;
if (log.isDebugEnabled()) {
log.debug("createClientSession - session={}", session);
}
session = null; // avoid auto-close at finally clause
return newSession;
} finally {
if (session != null) {
session.close();
}
}
}
protected ClientSession createClientSession(String hostname, String username, int port, long timeout) throws IOException {
SshClient client = getSshClient();
if (log.isDebugEnabled()) {
log.debug("createClientSession({}@{}:{}) waitTimeout={}", username, hostname, port, timeout);
}
ConnectFuture connectFuture = client.connect(username, hostname, port);
return connectFuture.verify(timeout).getSession();
}
protected ClientSession configureClientSessionProperties(ClientSession session, Properties props) throws IOException {
if (MapEntryUtils.isEmpty(props)) {
return session;
}
boolean debugEnabled = log.isDebugEnabled();
for (String propName : props.stringPropertyNames()) {
String propValue = props.getProperty(propName);
if (debugEnabled) {
log.debug("configureClientSessionProperties({}) set {}={}", session, propName, propValue);
}
PropertyResolverUtils.updateProperty(session, propName, propValue);
}
return session;
}
protected ClientSession authenticateClientSession(ClientSession session, long timeout)
throws IOException, GeneralSecurityException {
boolean debugEnabled = log.isDebugEnabled();
String passwordIdentity = getPassword();
if (GenericUtils.isNotEmpty(passwordIdentity)) {
if (debugEnabled) {
log.debug("authenticateClientSession({}) using password identity", session);
}
session.addPasswordIdentity(passwordIdentity);
}
KeyPair privateKeyIdentity = resolveKeyIdentity(session);
if (privateKeyIdentity != null) {
if (debugEnabled) {
PublicKey pubKey = privateKeyIdentity.getPublic();
log.debug("authenticateClientSession({}) using {} key={}",
session, KeyUtils.getKeyType(pubKey), KeyUtils.getFingerPrint(pubKey));
}
session.addPublicKeyIdentity(privateKeyIdentity);
}
if (debugEnabled) {
log.debug("authenticateClientSession({}) authenticate - timeout={}", session, timeout);
}
session.auth().verify(timeout);
return session;
}
protected long getEffectiveTimeoutValue(long timeoutSeconds) {
if (timeoutSeconds < (Long.MAX_VALUE / 61L)) {
return TimeUnit.SECONDS.toMillis(timeoutSeconds);
} else {
return timeoutSeconds;
}
}
}