/* * SFTPEnvironment.java * Copyright 2016 Rob Spoor * * 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 com.github.robtimus.filesystems.sftp; import java.io.File; import java.io.IOException; import java.net.Socket; import java.net.URI; import java.nio.file.FileStore; import java.nio.file.FileSystemException; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import com.github.robtimus.filesystems.FileSystemProviderSupport; import com.github.robtimus.filesystems.Messages; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.HostKeyRepository; import com.jcraft.jsch.IdentityRepository; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Proxy; import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpException; import com.jcraft.jsch.SocketFactory; import com.jcraft.jsch.UserInfo; /** * A utility class to set up environments that can be used in the {@link FileSystemProvider#newFileSystem(URI, Map)} and * {@link FileSystemProvider#newFileSystem(Path, Map)} methods of {@link SFTPFileSystemProvider}. * * @author Rob Spoor */ public class SFTPEnvironment implements Map<String, Object>, Cloneable { // session support private static final String USERNAME = "username"; //$NON-NLS-1$ // connect support private static final String CONNECT_TIMEOUT = "connectTimeout"; //$NON-NLS-1$ // JSch private static final String IDENTITY_REPOSITORY = "identityRepository"; //$NON-NLS-1$ private static final String IDENTITIES = "identities"; //$NON-NLS-1$ private static final String HOST_KEY_REPOSITORY = "hostKeyRepository"; //$NON-NLS-1$ private static final String KNOWN_HOSTS = "knownHosts"; //$NON-NLS-1$ // Session private static final String PROXY = "proxy"; //$NON-NLS-1$ private static final String USER_INFO = "userInfo"; //$NON-NLS-1$ private static final String PASSWORD = "password"; //$NON-NLS-1$ private static final String CONFIG = "config"; //$NON-NLS-1$ private static final String SOCKET_FACTORY = "socketFactory"; //$NON-NLS-1$ // timeOut should have been timeout, but that's a breaking change... private static final String TIMEOUT = "timeOut"; //$NON-NLS-1$ private static final String CLIENT_VERSION = "clientVersion"; //$NON-NLS-1$ private static final String HOST_KEY_ALIAS = "hostKeyAlias"; //$NON-NLS-1$ private static final String SERVER_ALIVE_INTERVAL = "serverAliveInterval"; //$NON-NLS-1$ private static final String SERVER_ALIVE_COUNT_MAX = "serverAliveCountMax"; //$NON-NLS-1$ // don't support port forwarding, X11 // ChannelSession private static final String AGENT_FORWARDING = "agentForwarding"; //$NON-NLS-1$ // don't support X11 forwarding, PTY or TTY // ChannelSftp private static final String FILENAME_ENCODING = "filenameEncoding"; //$NON-NLS-1$ // SFTP file system support private static final String DEFAULT_DIR = "defaultDir"; //$NON-NLS-1$ private static final int DEFAULT_CLIENT_CONNECTION_COUNT = 5; private static final long DEFAULT_CLIENT_CONNECTION_WAIT_TIMEOUT = 0; private static final String CLIENT_CONNECTION_COUNT = "clientConnectionCount"; //$NON-NLS-1$ private static final String CLIENT_CONNECTION_WAIT_TIMEOUT = "clientConnectionWaitTimeout"; //$NON-NLS-1$ private static final String FILE_SYSTEM_EXCEPTION_FACTORY = "fileSystemExceptionFactory"; //$NON-NLS-1$ private static final String CALCULATE_ACTUAL_TOTAL_SPACE = "calculateActualTotalSpace"; //$NON-NLS-1$ private Map<String, Object> map; /** * Creates a new SFTP environment. */ public SFTPEnvironment() { map = new HashMap<>(); } /** * Creates a new SFTP environment. * * @param map The map to wrap. */ public SFTPEnvironment(Map<String, Object> map) { this.map = Objects.requireNonNull(map); } @SuppressWarnings("unchecked") static SFTPEnvironment wrap(Map<String, ?> map) { if (map instanceof SFTPEnvironment) { return (SFTPEnvironment) map; } return new SFTPEnvironment((Map<String, Object>) map); } // session support /** * Stores the username to use. * * @param username The username to use. * @return This object. */ public SFTPEnvironment withUsername(String username) { put(USERNAME, username); return this; } // connect support /** * Stores the connection timeout to use. * * @param timeout The connection timeout in milliseconds. * @return This object. */ public SFTPEnvironment withConnectTimeout(int timeout) { put(CONNECT_TIMEOUT, timeout); return this; } // Session /** * Stores the proxy to use. * * @param proxy The proxy to use. * @return This object. */ public SFTPEnvironment withProxy(Proxy proxy) { put(PROXY, proxy); return this; } /** * Stores the user info to use. * * @param userInfo The user info to use. * @return This object. */ public SFTPEnvironment withUserInfo(UserInfo userInfo) { put(USER_INFO, userInfo); return this; } /** * Stores the password to use. * * @param password The password to use. * @return This object. * @since 1.1 */ public SFTPEnvironment withPassword(char[] password) { put(PASSWORD, password); return this; } /** * Stores configuration options to use. This method will add not clear any previously set options, but only add new ones. * * @param config The configuration options to use. * @return This object. * @throws NullPointerException if the given properties object is {@code null}. * @see #withConfig(String, String) */ public SFTPEnvironment withConfig(Properties config) { getConfig().putAll(config); return this; } /** * Stores a configuration option to use. This method will add not clear any previously set options, but only add new ones. * * @param key The configuration key. * @param value The configuration value. * @return This object. * @throws NullPointerException if the given key or value is {@code null}. * @see #withConfig(Properties) */ public SFTPEnvironment withConfig(String key, String value) { getConfig().setProperty(key, value); return this; } private Properties getConfig() { Properties config = FileSystemProviderSupport.getValue(this, CONFIG, Properties.class, null); if (config == null) { config = new Properties(); put(CONFIG, config); } return config; } /** * Stores the socket factory to use. * * @param factory The socket factory to use. * @return This object. */ public SFTPEnvironment withSocketFactory(SocketFactory factory) { put(SOCKET_FACTORY, factory); return this; } /** * Stores the timeout. * * @param timeout The timeout in milliseconds. * @return This object. * @see Socket#setSoTimeout(int) */ public SFTPEnvironment withTimeout(int timeout) { put(TIMEOUT, timeout); return this; } /** * Stores the client version to use. * * @param version The client version. * @return This object. */ public SFTPEnvironment withClientVersion(String version) { put(CLIENT_VERSION, version); return this; } /** * Stores the host key alias to use. * * @param alias The host key alias. * @return This object. */ public SFTPEnvironment withHostKeyAlias(String alias) { put(HOST_KEY_ALIAS, alias); return this; } /** * Stores the server alive interval to use. * * @param interval The server alive interval in milliseconds. * @return This object. */ public SFTPEnvironment withServerAliveInterval(int interval) { put(SERVER_ALIVE_INTERVAL, interval); return this; } /** * Stores the maximum number of server alive messages that can be sent without any reply before disconnecting. * * @param count The maximum number of server alive messages. * @return This object. */ public SFTPEnvironment withServerAliveCountMax(int count) { put(SERVER_ALIVE_COUNT_MAX, count); return this; } /** * Stores the identity repository to use. * * @param repository The identity repository to use. * @return This object. */ public SFTPEnvironment withIdentityRepository(IdentityRepository repository) { put(IDENTITY_REPOSITORY, repository); return this; } /** * Stores an identity to use. This method will add not clear any previously set identities, but only add new ones. * * @param identity The identity to use. * @return This object. * @throws NullPointerException If the given identity is {@code null}. * @see #withIdentities(Identity...) * @see #withIdentities(Collection) * @since 1.2 */ public SFTPEnvironment withIdentity(Identity identity) { Objects.requireNonNull(identity); getIdentities().add(identity); return this; } /** * Stores several identity to use. This method will add not clear any previously set identities, but only add new ones. * * @param identities The identities to use. * @return This object. * @throws NullPointerException If any of the given identity is {@code null}. * @see #withIdentity(Identity) * @see #withIdentities(Collection) * @since 1.2 */ public SFTPEnvironment withIdentities(Identity... identities) { Collection<Identity> existingIdentities = getIdentities(); for (Identity identity : identities) { Objects.requireNonNull(identity); existingIdentities.add(identity); } return this; } /** * Stores several identity to use. This method will add not clear any previously set identities, but only add new ones. * * @param identities The identities to use. * @return This object. * @throws NullPointerException If any of the given identity is {@code null}. * @see #withIdentity(Identity) * @see #withIdentities(Identity...) * @since 1.2 */ public SFTPEnvironment withIdentities(Collection<Identity> identities) { Collection<Identity> existingIdentities = getIdentities(); for (Identity identity : identities) { Objects.requireNonNull(identity); existingIdentities.add(identity); } return this; } private Collection<Identity> getIdentities() { @SuppressWarnings("unchecked") List<Identity> identities = FileSystemProviderSupport.getValue(this, IDENTITIES, List.class, null); if (identities == null) { identities = new ArrayList<>(); put(IDENTITIES, identities); } return identities; } /** * Stores the host key repository to use. * * @param repository The host key repository to use. * @return This object. */ public SFTPEnvironment withHostKeyRepository(HostKeyRepository repository) { put(HOST_KEY_REPOSITORY, repository); return this; } /** * Stores the known hosts file to use. * Note that the known hosts file is ignored if a {@link HostKeyRepository} is set with a non-{@code null} value. * * @param knownHosts The known hosts file to use. * @return This object. * @throws NullPointerException If the given file is {@code null}. * @see #withHostKeyRepository(HostKeyRepository) * @since 1.2 */ public SFTPEnvironment withKnownHosts(File knownHosts) { Objects.requireNonNull(knownHosts); put(KNOWN_HOSTS, knownHosts); return this; } /** * Stores whether or not agent forwarding should be enabled. * * @param agentForwarding {@code true} to enable strict agent forwarding, or {@code false} to disable it. * @return This object. */ public SFTPEnvironment withAgentForwarding(boolean agentForwarding) { put(AGENT_FORWARDING, agentForwarding); return this; } /** * Stores the filename encoding to use. * * @param encoding The filename encoding to use. * @return This object. */ public SFTPEnvironment withFilenameEncoding(String encoding) { put(FILENAME_ENCODING, encoding); return this; } // SFTP file system support /** * Stores the default directory to use. * If it exists, this will be the directory that relative paths are resolved to. * * @param pathname The default directory to use. * @return This object. */ public SFTPEnvironment withDefaultDirectory(String pathname) { put(DEFAULT_DIR, pathname); return this; } /** * Stores the number of client connections to use. This value influences the number of concurrent threads that can access an SFTP file system. * * @param count The number of client connection to use. * @return This object. */ public SFTPEnvironment withClientConnectionCount(int count) { put(CLIENT_CONNECTION_COUNT, count); return this; } /** * Stores the wait timeout to use for retrieving client connection from the connection pool. * <p> * If the timeout is not larger than {@code 0}, the SFTP file system waits indefinitely until a client connection becomes available. * * @param timeout The timeout in milliseconds. * @return This object. * @see #withClientConnectionWaitTimeout(long, TimeUnit) * @since 1.3 */ public SFTPEnvironment withClientConnectionWaitTimeout(long timeout) { put(CLIENT_CONNECTION_WAIT_TIMEOUT, timeout); return this; } /** * Stores the wait timeout to use for retrieving client connections from the connection pool. * <p> * If the timeout is not larger than {@code 0}, the SFTP file system waits indefinitely until a client connection becomes available. * * @param duration The timeout duration. * @param unit The timeout unit. * @return This object. * @throws NullPointerException If the timeout unit is {@code null}. * @see #withClientConnectionWaitTimeout(long) * @since 1.3 */ public SFTPEnvironment withClientConnectionWaitTimeout(long duration, TimeUnit unit) { return withClientConnectionWaitTimeout(TimeUnit.MILLISECONDS.convert(duration, unit)); } /** * Stores the file system exception factory to use. * * @param factory The file system exception factory to use. * @return This object. */ public SFTPEnvironment withFileSystemExceptionFactory(FileSystemExceptionFactory factory) { put(FILE_SYSTEM_EXCEPTION_FACTORY, factory); return this; } /** * Stores whether or not {@link FileStore#getTotalSpace()} should calculate the actual total space by traversing the file system. * If not explicitly set to {@code true}, the method will return {@link Long#MAX_VALUE} instead. * * @param calculateActualTotalSpace {@code true} if {@link FileStore#getTotalSpace()} should calculate the actual total space by traversing the * file system, or {@code false} otherwise. * @return This object. * @deprecated {@link FileStore#getTotalSpace()} does not need to traverse the file system, because that would calculate the total <em>used</em> * space, not the total space. */ @Deprecated public SFTPEnvironment withActualTotalSpaceCalculation(boolean calculateActualTotalSpace) { put(CALCULATE_ACTUAL_TOTAL_SPACE, calculateActualTotalSpace); return this; } String getUsername() { return FileSystemProviderSupport.getValue(this, USERNAME, String.class, null); } int getClientConnectionCount() { int count = FileSystemProviderSupport.getIntValue(this, CLIENT_CONNECTION_COUNT, DEFAULT_CLIENT_CONNECTION_COUNT); return Math.max(1, count); } long getClientConnectionWaitTimeout() { long timeout = FileSystemProviderSupport.getLongValue(this, CLIENT_CONNECTION_WAIT_TIMEOUT, DEFAULT_CLIENT_CONNECTION_WAIT_TIMEOUT); return Math.max(0, timeout); } FileSystemExceptionFactory getExceptionFactory() { return FileSystemProviderSupport.getValue(this, FILE_SYSTEM_EXCEPTION_FACTORY, FileSystemExceptionFactory.class, DefaultFileSystemExceptionFactory.INSTANCE); } JSch createJSch() throws IOException { JSch jsch = new JSch(); initialize(jsch); return jsch; } void initialize(JSch jsch) throws IOException { if (containsKey(IDENTITY_REPOSITORY)) { IdentityRepository identityRepository = FileSystemProviderSupport.getValue(this, IDENTITY_REPOSITORY, IdentityRepository.class, null); jsch.setIdentityRepository(identityRepository); } if (containsKey(IDENTITIES)) { Collection<?> identities = FileSystemProviderSupport.getValue(this, IDENTITIES, Collection.class); for (Object o : identities) { if (o instanceof Identity) { Identity identity = (Identity) o; try { identity.addIdentity(jsch); } catch (JSchException e) { throw asFileSystemException(e); } } else { throw Messages.fileSystemProvider().env().invalidProperty(IDENTITIES, identities); } } } if (containsKey(HOST_KEY_REPOSITORY)) { HostKeyRepository hostKeyRepository = FileSystemProviderSupport.getValue(this, HOST_KEY_REPOSITORY, HostKeyRepository.class, null); jsch.setHostKeyRepository(hostKeyRepository); } if (containsKey(KNOWN_HOSTS)) { File knownHosts = FileSystemProviderSupport.getValue(this, KNOWN_HOSTS, File.class); try { jsch.setKnownHosts(knownHosts.getAbsolutePath()); } catch (JSchException e) { throw asFileSystemException(e); } } } ChannelSftp openChannel(JSch jsch, String hostname, int port) throws IOException { Session session = getSession(jsch, hostname, port); try { initialize(session); ChannelSftp channel = connect(session); try { initializePreConnect(channel); connect(channel); initializePostConnect(channel); verifyConnection(channel); return channel; } catch (IOException e) { channel.disconnect(); throw e; } } catch (IOException e) { session.disconnect(); throw e; } } Session getSession(JSch jsch, String hostname, int port) throws IOException { String username = getUsername(); try { return jsch.getSession(username, hostname, port == -1 ? 22 : port); } catch (JSchException e) { throw asFileSystemException(e); } } void initialize(Session session) throws IOException { if (containsKey(PROXY)) { Proxy proxy = FileSystemProviderSupport.getValue(this, PROXY, Proxy.class, null); session.setProxy(proxy); } if (containsKey(USER_INFO)) { UserInfo userInfo = FileSystemProviderSupport.getValue(this, USER_INFO, UserInfo.class, null); session.setUserInfo(userInfo); } if (containsKey(PASSWORD)) { char[] password = FileSystemProviderSupport.getValue(this, PASSWORD, char[].class, null); session.setPassword(password == null ? null : new String(password)); } if (containsKey(CONFIG)) { Properties config = FileSystemProviderSupport.getValue(this, CONFIG, Properties.class, null); session.setConfig(config); } if (containsKey(SOCKET_FACTORY)) { SocketFactory socketFactory = FileSystemProviderSupport.getValue(this, SOCKET_FACTORY, SocketFactory.class, null); session.setSocketFactory(socketFactory); } if (containsKey(TIMEOUT)) { int timeout = FileSystemProviderSupport.getIntValue(this, TIMEOUT); try { session.setTimeout(timeout); } catch (JSchException e) { throw asFileSystemException(e); } } if (containsKey(CLIENT_VERSION)) { String clientVersion = FileSystemProviderSupport.getValue(this, CLIENT_VERSION, String.class, null); session.setClientVersion(clientVersion); } if (containsKey(HOST_KEY_ALIAS)) { String hostKeyAlias = FileSystemProviderSupport.getValue(this, HOST_KEY_ALIAS, String.class, null); session.setHostKeyAlias(hostKeyAlias); } if (containsKey(SERVER_ALIVE_INTERVAL)) { int interval = FileSystemProviderSupport.getIntValue(this, SERVER_ALIVE_INTERVAL); try { session.setServerAliveInterval(interval); } catch (JSchException e) { throw asFileSystemException(e); } } if (containsKey(SERVER_ALIVE_COUNT_MAX)) { int count = FileSystemProviderSupport.getIntValue(this, SERVER_ALIVE_COUNT_MAX); session.setServerAliveCountMax(count); } } ChannelSftp connect(Session session) throws IOException { try { if (containsKey(CONNECT_TIMEOUT)) { int connectTimeout = FileSystemProviderSupport.getIntValue(this, CONNECT_TIMEOUT); session.connect(connectTimeout); } else { session.connect(); } return (ChannelSftp) session.openChannel("sftp"); //$NON-NLS-1$ } catch (JSchException e) { throw asFileSystemException(e); } } void initializePreConnect(ChannelSftp channel) throws IOException { if (containsKey(AGENT_FORWARDING)) { boolean forwarding = FileSystemProviderSupport.getBooleanValue(this, AGENT_FORWARDING); channel.setAgentForwarding(forwarding); } if (containsKey(FILENAME_ENCODING)) { String filenameEncoding = FileSystemProviderSupport.getValue(this, FILENAME_ENCODING, String.class, null); try { channel.setFilenameEncoding(filenameEncoding); } catch (SftpException e) { throw asFileSystemException(e); } } } void connect(ChannelSftp channel) throws IOException { try { if (containsKey(CONNECT_TIMEOUT)) { int connectTimeout = FileSystemProviderSupport.getIntValue(this, CONNECT_TIMEOUT); channel.connect(connectTimeout); } else { channel.connect(); } } catch (JSchException e) { throw asFileSystemException(e); } } void initializePostConnect(ChannelSftp channel) throws IOException { String defaultDir = FileSystemProviderSupport.getValue(this, DEFAULT_DIR, String.class, null); if (defaultDir != null) { try { channel.cd(defaultDir); } catch (SftpException e) { throw getExceptionFactory().createChangeWorkingDirectoryException(defaultDir, e); } } } void verifyConnection(ChannelSftp channel) throws IOException { try { channel.pwd(); } catch (SftpException e) { throw asFileSystemException(e); } } FileSystemException asFileSystemException(Exception e) throws FileSystemException { if (e instanceof FileSystemException) { throw (FileSystemException) e; } FileSystemException exception = new FileSystemException(null, null, e.getMessage()); exception.initCause(e); throw exception; } // Map / Object @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public boolean containsKey(Object key) { return map.containsKey(key); } @Override public boolean containsValue(Object value) { return map.containsValue(value); } @Override public Object get(Object key) { return map.get(key); } @Override public Object put(String key, Object value) { return map.put(key, value); } @Override public Object remove(Object key) { return map.remove(key); } @Override public void putAll(Map<? extends String, ? extends Object> m) { map.putAll(m); } @Override public void clear() { map.clear(); } @Override public Set<String> keySet() { return map.keySet(); } @Override public Collection<Object> values() { return map.values(); } @Override public Set<Map.Entry<String, Object>> entrySet() { return map.entrySet(); } @Override public boolean equals(Object o) { return map.equals(o); } @Override public int hashCode() { return map.hashCode(); } @Override public String toString() { return map.toString(); } @Override public SFTPEnvironment clone() { try { SFTPEnvironment clone = (SFTPEnvironment) super.clone(); clone.map = new HashMap<>(map); return clone; } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } } }