org.springframework.integration.sftp.session.DefaultSftpSessionFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.sftp.session.DefaultSftpSessionFactory.java

Source

/*
 * Copyright 2002-2019 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
 *
 *      https://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.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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.integration.util.JavaUtils;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
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 UserInfo userInfoWrapper = new UserInfoWrapper();

    private final JSch jsch;

    private final boolean isSharedSession;

    private final Lock sharedSessionLock;

    private String host;

    private int port = 22; // the default

    private String user;

    private String password;

    private String knownHosts;

    private Resource privateKey;

    private String privateKeyPassphrase;

    private Properties sessionConfig;

    private Proxy proxy;

    private SocketFactory socketFactory;

    private Integer timeout;

    private String clientVersion;

    private String hostKeyAlias;

    private Integer serverAliveInterval;

    private Integer serverAliveCountMax;

    private Boolean enableDaemonThread;

    private UserInfo userInfo;

    private boolean allowUnknownKeys = false;

    private Duration channelConnectTimeout;

    private volatile JSchSessionWrapper sharedJschSession;

    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;
        if (this.isSharedSession) {
            this.sharedSessionLock = new ReentrantLock();
        } else {
            this.sharedSessionLock = null;
        }
    }

    /**
     * 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;
    }

    /**
     * Set the connect timeout.
     * @param timeout the timeout to set.
     * @since 5.2
     */
    public void setChannelConnectTimeout(Duration timeout) {
        Assert.notNull(timeout, "'connectTimeout' cannot be null");
        this.channelConnectTimeout = timeout;
    }

    @Override
    public SftpSession getSession() {
        JSchSessionWrapper jschSession = this.sharedJschSession;
        SftpSession sftpSession;
        if (this.sharedSessionLock != null) {
            this.sharedSessionLock.lock();
        }
        try {
            boolean freshJschSession = false;
            if (jschSession == null || !jschSession.isConnected()) {
                jschSession = new JSchSessionWrapper(initJschSession());
                freshJschSession = true;
            }
            sftpSession = new SftpSession(jschSession);
            JavaUtils.INSTANCE.acceptIfNotNull(this.channelConnectTimeout, sftpSession::setChannelConnectTimeout);
            sftpSession.connect();
            if (this.isSharedSession && freshJschSession) {
                this.sharedJschSession = jschSession;
            }
        } catch (Exception e) {
            throw new IllegalStateException("failed to create SFTP Session", e);
        } finally {
            if (this.sharedSessionLock != null) {
                this.sharedSessionLock.unlock();
            }
        }
        jschSession.addChannel();
        return sftpSession;
    }

    private com.jcraft.jsch.Session initJschSession() throws JSchException, IOException {
        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");

        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 = FileCopyUtils.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);
        JavaUtils.INSTANCE.acceptIfNotNull(this.sessionConfig, jschSession::setConfig)
                .acceptIfHasText(this.userInfoWrapper.getPassword(), jschSession::setPassword);
        jschSession.setUserInfo(this.userInfoWrapper);

        try {
            if (this.timeout != null) {
                jschSession.setTimeout(this.timeout);
            }
            if (this.serverAliveInterval != null) {
                jschSession.setServerAliveInterval(this.serverAliveInterval);
            }
            JavaUtils.INSTANCE.acceptIfNotNull(this.proxy, jschSession::setProxy)
                    .acceptIfNotNull(this.socketFactory, jschSession::setSocketFactory)
                    .acceptIfHasText(this.clientVersion, jschSession::setClientVersion)
                    .acceptIfHasText(this.hostKeyAlias, jschSession::setHostKeyAlias)
                    .acceptIfNotNull(this.serverAliveCountMax, jschSession::setServerAliveCountMax)
                    .acceptIfNotNull(this.enableDaemonThread, jschSession::setDaemonThread);
        } 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.toString(echo));
                }
                return null;
            }
        }

    }

}