/* * Copyright 2002-2016 the original author or authors. * * Licensed 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.springframework.integration.sftp.session; import java.util.Arrays; import java.util.Properties; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanCreationException; import org.springframework.core.io.Resource; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.remote.session.SharedSessionCapable; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import com.jcraft.jsch.ChannelSftp.LsEntry; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Proxy; import com.jcraft.jsch.SocketFactory; import com.jcraft.jsch.UIKeyboardInteractive; import com.jcraft.jsch.UserInfo; /** * Factory for creating {@link SftpSession} instances. * * @author Josh Long * @author Mario Gray * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell * @author David Liu * @author Pat Turner * @author Artem Bilan * * @since 2.0 */ public class DefaultSftpSessionFactory implements SessionFactory<LsEntry>, SharedSessionCapable { private static final Log logger = LogFactory.getLog(DefaultSftpSessionFactory.class); static { JSch.setLogger(new JschLogger()); } private final ReadWriteLock sharedSessionLock = new ReentrantReadWriteLock(); private final UserInfo userInfoWrapper = new UserInfoWrapper(); private final JSch jsch; private final boolean isSharedSession; private volatile String host; private volatile int port = 22; // the default private volatile String user; private volatile String password; private volatile String knownHosts; private volatile Resource privateKey; private volatile String privateKeyPassphrase; private volatile Properties sessionConfig; private volatile Proxy proxy; private volatile SocketFactory socketFactory; private volatile Integer timeout; private volatile String clientVersion; private volatile String hostKeyAlias; private volatile Integer serverAliveInterval; private volatile Integer serverAliveCountMax; private volatile Boolean enableDaemonThread; private volatile JSchSessionWrapper sharedJschSession; private volatile UserInfo userInfo; private volatile boolean allowUnknownKeys = false; public DefaultSftpSessionFactory() { this(false); } /** * @param isSharedSession true if the session is to be shared. */ public DefaultSftpSessionFactory(boolean isSharedSession) { this(new JSch(), isSharedSession); } /** * Intended for use in tests so the jsch can be mocked. * @param jsch The jsch instance. * @param isSharedSession true if the session is to be shared. */ public DefaultSftpSessionFactory(JSch jsch, boolean isSharedSession) { this.jsch = jsch; this.isSharedSession = isSharedSession; } /** * The url of the host you want connect to. This is a mandatory property. * @param host The host. * @see JSch#getSession(String, String, int) */ public void setHost(String host) { this.host = host; } /** * The port over which the SFTP connection shall be established. If not specified, * this value defaults to <code>22</code>. If specified, this properties must * be a positive number. * @param port The port. * @see JSch#getSession(String, String, int) */ public void setPort(int port) { this.port = port; } /** * The remote user to use. This is a mandatory property. * @param user The user. * @see JSch#getSession(String, String, int) */ public void setUser(String user) { this.user = user; } /** * The password to authenticate against the remote host. If a password is * not provided, then a {@link DefaultSftpSessionFactory#setPrivateKey(Resource) privateKey} is * mandatory. * Not allowed if {@link #setUserInfo(UserInfo) userInfo} is provided - the password is obtained * from that object. * @param password The password. * @see com.jcraft.jsch.Session#setPassword(String) */ public void setPassword(String password) { this.password = password; } /** * Specifies the filename that will be used for a host key repository. * The file has the same format as OpenSSH's known_hosts file. * <p> * <b>Required if {@link #setAllowUnknownKeys(boolean) allowUnknownKeys} is * false (default).</b> * @param knownHosts The known hosts. * @see JSch#setKnownHosts(String) */ public void setKnownHosts(String knownHosts) { this.knownHosts = knownHosts; } /** * 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 DefaultSftpSessionFactory#setPassword(String) password} * property is mandatory (or {@link #setUserInfo(UserInfo) userInfo} that returns a * password. * @param privateKey The private key. * @see JSch#addIdentity(String) * @see JSch#addIdentity(String, String) */ public void setPrivateKey(Resource privateKey) { this.privateKey = privateKey; } /** * The password for the private key. Optional. * Not allowed if {@link #setUserInfo(UserInfo) userInfo} is provided - the passphrase is obtained * from that object. * @param privateKeyPassphrase The private key passphrase. * @see JSch#addIdentity(String, String) */ public void setPrivateKeyPassphrase(String privateKeyPassphrase) { this.privateKeyPassphrase = privateKeyPassphrase; } /** * Using {@link Properties}, you can set additional configuration settings on * the underlying JSch {@link com.jcraft.jsch.Session}. * @param sessionConfig The session configuration properties. * @see com.jcraft.jsch.Session#setConfig(Properties) */ public void setSessionConfig(Properties sessionConfig) { this.sessionConfig = sessionConfig; } /** * Allows for specifying a JSch-based {@link Proxy}. If set, then the proxy * object is used to create the connection to the remote host. * @param proxy The proxy. * @see com.jcraft.jsch.Session#setProxy(Proxy) */ public void setProxy(Proxy proxy) { this.proxy = proxy; } /** * Allows you to pass in a {@link SocketFactory}. The socket factory is used * to create a socket to the target host. When a {@link Proxy} is used, the * socket factory is passed to the proxy. By default plain TCP sockets are used. * @param socketFactory The socket factory. * @see com.jcraft.jsch.Session#setSocketFactory(SocketFactory) */ public void setSocketFactory(SocketFactory socketFactory) { this.socketFactory = socketFactory; } /** * The timeout property is used as the socket timeout parameter, as well as * the default connection timeout. Defaults to <code>0</code>, which means, * that no timeout will occur. * @param timeout The timeout. * @see com.jcraft.jsch.Session#setTimeout(int) */ public void setTimeout(Integer timeout) { this.timeout = timeout; } /** * Allows you to set the client version property. It's default depends on the * underlying JSch version but it will look like <code>SSH-2.0-JSCH-0.1.45</code> * @param clientVersion The client version. * @see com.jcraft.jsch.Session#setClientVersion(String) */ public void setClientVersion(String clientVersion) { this.clientVersion = clientVersion; } /** * Sets the host key alias, used when comparing the host key to the known * hosts list. * @param hostKeyAlias The host key alias. * @see com.jcraft.jsch.Session#setHostKeyAlias(String) */ public void setHostKeyAlias(String hostKeyAlias) { this.hostKeyAlias = hostKeyAlias; } /** * Sets the timeout interval (milliseconds) before a server alive message is * sent, in case no message is received from the server. * @param serverAliveInterval The server alive interval. * @see com.jcraft.jsch.Session#setServerAliveInterval(int) */ public void setServerAliveInterval(Integer serverAliveInterval) { this.serverAliveInterval = serverAliveInterval; } /** * Specifies the number of server-alive messages, which will be sent without * any reply from the server before disconnecting. If not set, this property * defaults to <code>1</code>. * @param serverAliveCountMax The server alive count max. * @see com.jcraft.jsch.Session#setServerAliveCountMax(int) */ public void setServerAliveCountMax(Integer serverAliveCountMax) { this.serverAliveCountMax = serverAliveCountMax; } /** * If true, all threads will be daemon threads. If set to <code>false</code>, * normal non-daemon threads will be used. This property will be set on the * underlying {@link com.jcraft.jsch.Session} using * {@link com.jcraft.jsch.Session#setDaemonThread(boolean)}. There, this * property will default to <code>false</code>, if not explicitly set. * @param enableDaemonThread true to enable a daemon thread. * @see com.jcraft.jsch.Session#setDaemonThread(boolean) */ public void setEnableDaemonThread(Boolean enableDaemonThread) { this.enableDaemonThread = enableDaemonThread; } /** * Provide a {@link UserInfo} which exposes control over dealing with new keys or key * changes. As Spring Integration will not normally allow user interaction, the * implementation must respond to Jsch calls in a suitable way. * <p> * Jsch calls {@link UserInfo#promptYesNo(String)} when connecting to an unknown host, * or when a known host's key has changed (see {@link #setKnownHosts(String) * knownHosts}). Generally, it should return false as returning true will accept all * new keys or key changes. * <p> * If no {@link UserInfo} is provided, the behavior is defined by * {@link #setAllowUnknownKeys(boolean) allowUnknownKeys}. * <p> * If {@link #setPassword(String) setPassword} is invoked with a non-null password, it will * override any password in the supplied {@link UserInfo}. * <p> * <b>NOTE: When this is provided, the {@link #setPassword(String) password} and * {@link #setPrivateKeyPassphrase(String) passphrase} are not allowed because those values * will be obtained from the {@link UserInfo}.</b> * @param userInfo the UserInfo. * @see com.jcraft.jsch.Session#setUserInfo(com.jcraft.jsch.UserInfo) * @since 4.1.7 */ public void setUserInfo(UserInfo userInfo) { this.userInfo = userInfo; } /** * When no {@link UserInfo} has been provided, set to true to unconditionally allow * connecting to an unknown host or when a host's key has changed (see * {@link #setKnownHosts(String) knownHosts}). Default false (since 4.2). * Set to true if a knownHosts file is not provided. * @param allowUnknownKeys true to allow connecting to unknown hosts. * @since 4.1.7 */ public void setAllowUnknownKeys(boolean allowUnknownKeys) { this.allowUnknownKeys = allowUnknownKeys; } @Override public SftpSession getSession() { Assert.hasText(this.host, "host must not be empty"); Assert.hasText(this.user, "user must not be empty"); Assert.isTrue(StringUtils.hasText(this.userInfoWrapper.getPassword()) || this.privateKey != null, "either a password or a private key is required"); try { JSchSessionWrapper jschSession; if (this.isSharedSession) { this.sharedSessionLock.readLock().lock(); try { if (this.sharedJschSession == null || !this.sharedJschSession.isConnected()) { this.sharedSessionLock.readLock().unlock(); this.sharedSessionLock.writeLock().lock(); try { if (this.sharedJschSession == null || !this.sharedJschSession.isConnected()) { this.sharedJschSession = new JSchSessionWrapper(initJschSession()); try { this.sharedJschSession.getSession().connect(); } catch (JSchException e) { throw new IllegalStateException("failed to connect", e); } } } finally { this.sharedSessionLock.readLock().lock(); this.sharedSessionLock.writeLock().unlock(); } } } finally { this.sharedSessionLock.readLock().unlock(); } jschSession = this.sharedJschSession; } else { jschSession = new JSchSessionWrapper(initJschSession()); } SftpSession sftpSession = new SftpSession(jschSession); sftpSession.connect(); jschSession.addChannel(); return sftpSession; } catch (Exception e) { throw new IllegalStateException("failed to create SFTP Session", e); } } private com.jcraft.jsch.Session initJschSession() throws Exception { if (this.port <= 0) { this.port = 22; } if (StringUtils.hasText(this.knownHosts)) { this.jsch.setKnownHosts(this.knownHosts); } // private key if (this.privateKey != null) { byte[] keyByteArray = StreamUtils.copyToByteArray(this.privateKey.getInputStream()); String passphrase = this.userInfoWrapper.getPassphrase(); if (StringUtils.hasText(passphrase)) { this.jsch.addIdentity(this.user, keyByteArray, null, passphrase.getBytes()); } else { this.jsch.addIdentity(this.user, keyByteArray, null, null); } } com.jcraft.jsch.Session jschSession = this.jsch.getSession(this.user, this.host, this.port); if (this.sessionConfig != null) { jschSession.setConfig(this.sessionConfig); } String password = this.userInfoWrapper.getPassword(); if (StringUtils.hasText(password)) { jschSession.setPassword(password); } jschSession.setUserInfo(this.userInfoWrapper); try { if (this.proxy != null) { jschSession.setProxy(this.proxy); } if (this.socketFactory != null) { jschSession.setSocketFactory(this.socketFactory); } if (this.timeout != null) { jschSession.setTimeout(this.timeout); } if (StringUtils.hasText(this.clientVersion)) { jschSession.setClientVersion(this.clientVersion); } if (StringUtils.hasText(this.hostKeyAlias)) { jschSession.setHostKeyAlias(this.hostKeyAlias); } if (this.serverAliveInterval != null) { jschSession.setServerAliveInterval(this.serverAliveInterval); } if (this.serverAliveCountMax != null) { jschSession.setServerAliveCountMax(this.serverAliveCountMax); } if (this.enableDaemonThread != null) { jschSession.setDaemonThread(this.enableDaemonThread); } } catch (Exception e) { throw new BeanCreationException("Attempt to set additional properties of " + "the com.jcraft.jsch.Session resulted in error: " + e.getMessage(), e); } return jschSession; } @Override public final boolean isSharedSession() { return this.isSharedSession; } @Override public void resetSharedSession() { Assert.state(this.isSharedSession, "Shared sessions are not being used"); this.sharedJschSession = null; } /** * Wrapper class will delegate calls to a configured {@link UserInfo}, providing * sensible defaults if null. As the password is configured in this Factory, the * wrapper will return the factory's configured password and only delegate to the * UserInfo if null. * @since 4.1.7 */ private class UserInfoWrapper implements UserInfo, UIKeyboardInteractive { UserInfoWrapper() { super(); } /** * Convenience to check whether enclosing factory's UserInfo is configured. * @return true if there's a delegate. */ private boolean hasDelegate() { return getDelegate() != null; } /** * Convenience to retrieve enclosing factory's UserInfo. * @return the {@link #userInfo} or null if not present. */ private UserInfo getDelegate() { return DefaultSftpSessionFactory.this.userInfo; } @Override public String getPassphrase() { if (hasDelegate()) { Assert.state(!StringUtils.hasText(DefaultSftpSessionFactory.this.privateKeyPassphrase), "When a 'UserInfo' is provided, 'privateKeyPassphrase' is not allowed"); return getDelegate().getPassphrase(); } else { return DefaultSftpSessionFactory.this.privateKeyPassphrase; } } @Override public String getPassword() { if (hasDelegate()) { Assert.state(!StringUtils.hasText(DefaultSftpSessionFactory.this.password), "When a 'UserInfo' is provided, 'password' is not allowed"); return getDelegate().getPassword(); } else { return DefaultSftpSessionFactory.this.password; } } @Override public boolean promptPassword(String message) { if (hasDelegate()) { return getDelegate().promptPassword(message); } else { if (logger.isDebugEnabled()) { logger.debug("No UserInfo provided - " + message + ", returning: true"); } return true; } } @Override public boolean promptPassphrase(String message) { if (hasDelegate()) { return getDelegate().promptPassphrase(message); } else { if (logger.isDebugEnabled()) { logger.debug("No UserInfo provided - " + message + ", returning: true"); } return true; } } @Override public boolean promptYesNo(String message) { logger.info(message); if (hasDelegate()) { return getDelegate().promptYesNo(message); } else { if (logger.isDebugEnabled()) { logger.debug("No UserInfo provided - " + message + ", returning:" + DefaultSftpSessionFactory.this.allowUnknownKeys); } return DefaultSftpSessionFactory.this.allowUnknownKeys; } } @Override public void showMessage(String message) { if (hasDelegate()) { getDelegate().showMessage(message); } else { logger.debug(message); } } @Override public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) { if (hasDelegate() && getDelegate() instanceof UIKeyboardInteractive) { return ((UIKeyboardInteractive) getDelegate()).promptKeyboardInteractive(destination, name, instruction, prompt, echo); } else { if (logger.isDebugEnabled()) { logger.debug("No UIKeyboardInteractive provided - " + destination + ":" + name + ":" + instruction + ":" + Arrays.asList(prompt) + ":" + Arrays.asList(echo)); } return null; } } } }