001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.sftp; 018 019import java.io.File; 020import java.io.IOException; 021import java.time.Duration; 022import java.util.Objects; 023import java.util.Properties; 024 025import org.apache.commons.lang3.SystemProperties; 026import org.apache.commons.lang3.SystemUtils; 027import org.apache.commons.lang3.time.DurationUtils; 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030import org.apache.commons.vfs2.FileSystemException; 031import org.apache.commons.vfs2.FileSystemOptions; 032 033import com.jcraft.jsch.ConfigRepository; 034import com.jcraft.jsch.JSch; 035import com.jcraft.jsch.JSchException; 036import com.jcraft.jsch.Logger; 037import com.jcraft.jsch.OpenSSHConfig; 038import com.jcraft.jsch.Proxy; 039import com.jcraft.jsch.ProxyHTTP; 040import com.jcraft.jsch.ProxySOCKS5; 041import com.jcraft.jsch.Session; 042import com.jcraft.jsch.UserInfo; 043 044/** 045 * Create a JSch Session instance. 046 */ 047public final class SftpClientFactory { 048 049 /** Interface JSchLogger with JCL. */ 050 private static final class JSchLogger implements Logger { 051 @Override 052 public boolean isEnabled(final int level) { 053 switch (level) { 054 case FATAL: 055 return LOG.isFatalEnabled(); 056 case ERROR: 057 return LOG.isErrorEnabled(); 058 case WARN: 059 return LOG.isDebugEnabled(); 060 case DEBUG: 061 return LOG.isDebugEnabled(); 062 case INFO: 063 return LOG.isInfoEnabled(); 064 default: 065 return LOG.isDebugEnabled(); 066 } 067 } 068 069 @Override 070 public void log(final int level, final String msg) { 071 switch (level) { 072 case FATAL: 073 LOG.fatal(msg); 074 break; 075 case ERROR: 076 LOG.error(msg); 077 break; 078 case WARN: 079 LOG.warn(msg); 080 break; 081 case DEBUG: 082 LOG.debug(msg); 083 break; 084 case INFO: 085 LOG.info(msg); 086 break; 087 default: 088 LOG.debug(msg); 089 } 090 } 091 } 092 private static final String KEY_COMPRESSION_C2S = "compression.c2s"; 093 private static final String KEY_COMPRESSION_S2C = "compression.s2c"; 094 private static final String KEY_PREFERRED_AUTHENTICATIONS = "PreferredAuthentications"; 095 096 private static final String KEY_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; 097 098 private static final String SSH_DIR_NAME = ".ssh"; 099 private static final String OPENSSH_CONFIG_NAME = "config"; 100 private static final Log LOG = LogFactory.getLog(SftpClientFactory.class); 101 102 static { 103 JSch.setLogger(new JSchLogger()); 104 } 105 106 private static void addIdentities(final JSch jsch, final File sshDir, final IdentityProvider[] identities) 107 throws FileSystemException { 108 if (identities != null) { 109 for (final IdentityProvider info : identities) { 110 addIdentity(jsch, info); 111 } 112 } else { 113 // Load the private key (rsa-key only) 114 final File privateKeyFile = new File(sshDir, "id_rsa"); 115 if (privateKeyFile.isFile() && privateKeyFile.canRead()) { 116 addIdentity(jsch, new IdentityInfo(privateKeyFile)); 117 } 118 } 119 } 120 121 private static void addIdentity(final JSch jsch, final IdentityProvider identity) throws FileSystemException { 122 try { 123 identity.addIdentity(jsch); 124 } catch (final JSchException e) { 125 throw new FileSystemException("vfs.provider.sftp/load-private-key.error", identity, e); 126 } 127 } 128 129 /** 130 * Creates a new connection to the server. 131 * 132 * @param hostname The name of the host to connect to. 133 * @param port The port to use. 134 * @param username The user's id. 135 * @param password The user's password. 136 * @param fileSystemOptions The FileSystem options. 137 * @return A Session, never null. 138 * @throws FileSystemException if an error occurs. 139 */ 140 public static Session createConnection(final String hostname, final int port, final char[] username, 141 final char[] password, final FileSystemOptions fileSystemOptions) throws FileSystemException { 142 Objects.requireNonNull(username, "username"); 143 final JSch jsch = new JSch(); 144 145 // new style - user passed 146 final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance(); 147 final File knownHostsFile = builder.getKnownHosts(fileSystemOptions); 148 final IdentityProvider[] identities = builder.getIdentityProvider(fileSystemOptions); 149 final IdentityRepositoryFactory repositoryFactory = builder.getIdentityRepositoryFactory(fileSystemOptions); 150 final ConfigRepository configRepository = builder.getConfigRepository(fileSystemOptions); 151 final boolean loadOpenSSHConfig = builder.isLoadOpenSSHConfig(fileSystemOptions); 152 153 final File sshDir = findSshDir(); 154 155 setKnownHosts(jsch, sshDir, knownHostsFile); 156 157 if (repositoryFactory != null) { 158 jsch.setIdentityRepository(repositoryFactory.create(jsch)); 159 } 160 161 addIdentities(jsch, sshDir, identities); 162 setConfigRepository(jsch, sshDir, configRepository, loadOpenSSHConfig); 163 164 final Session session; 165 try { 166 session = jsch.getSession(new String(username), hostname, port); 167 if (password != null) { 168 session.setPassword(new String(password)); 169 } 170 171 final Duration sessionTimeout = builder.getSessionTimeout(fileSystemOptions); 172 if (sessionTimeout != null) { 173 session.setTimeout(DurationUtils.toMillisInt(sessionTimeout)); 174 } 175 176 final UserInfo userInfo = builder.getUserInfo(fileSystemOptions); 177 if (userInfo != null) { 178 session.setUserInfo(userInfo); 179 } 180 181 final Properties config = new Properties(); 182 183 // set StrictHostKeyChecking property 184 final String strictHostKeyChecking = builder.getStrictHostKeyChecking(fileSystemOptions); 185 if (strictHostKeyChecking != null) { 186 config.setProperty(KEY_STRICT_HOST_KEY_CHECKING, strictHostKeyChecking); 187 } 188 // set PreferredAuthentications property 189 final String preferredAuthentications = builder.getPreferredAuthentications(fileSystemOptions); 190 if (preferredAuthentications != null) { 191 config.setProperty(KEY_PREFERRED_AUTHENTICATIONS, preferredAuthentications); 192 } 193 194 // set compression property 195 final String compression = builder.getCompression(fileSystemOptions); 196 if (compression != null) { 197 config.setProperty(KEY_COMPRESSION_S2C, compression); 198 config.setProperty(KEY_COMPRESSION_C2S, compression); 199 } 200 201 final String keyExchangeAlgorithm = builder.getKeyExchangeAlgorithm(fileSystemOptions); 202 if (keyExchangeAlgorithm != null) { 203 config.setProperty("kex", keyExchangeAlgorithm); 204 } 205 206 final String proxyHost = builder.getProxyHost(fileSystemOptions); 207 if (proxyHost != null) { 208 final int proxyPort = builder.getProxyPort(fileSystemOptions); 209 final SftpFileSystemConfigBuilder.ProxyType proxyType = builder.getProxyType(fileSystemOptions); 210 final String proxyUser = builder.getProxyUser(fileSystemOptions); 211 final String proxyPassword = builder.getProxyPassword(fileSystemOptions); 212 Proxy proxy = null; 213 if (SftpFileSystemConfigBuilder.PROXY_HTTP.equals(proxyType)) { 214 proxy = createProxyHTTP(proxyHost, proxyPort); 215 ((ProxyHTTP) proxy).setUserPasswd(proxyUser, proxyPassword); 216 } else if (SftpFileSystemConfigBuilder.PROXY_SOCKS5.equals(proxyType)) { 217 proxy = createProxySOCKS5(proxyHost, proxyPort); 218 ((ProxySOCKS5) proxy).setUserPasswd(proxyUser, proxyPassword); 219 } else if (SftpFileSystemConfigBuilder.PROXY_STREAM.equals(proxyType)) { 220 proxy = createStreamProxy(proxyHost, proxyPort, fileSystemOptions, builder); 221 } 222 223 if (proxy != null) { 224 session.setProxy(proxy); 225 } 226 } 227 228 // set properties for the session 229 if (!config.isEmpty()) { 230 session.setConfig(config); 231 } 232 session.setDaemonThread(true); 233 session.connect(); 234 } catch (final Exception exc) { 235 throw new FileSystemException("vfs.provider.sftp/connect.error", exc, hostname); 236 } 237 238 return session; 239 } 240 241 private static ProxyHTTP createProxyHTTP(final String proxyHost, final int proxyPort) { 242 return proxyPort == 0 ? new ProxyHTTP(proxyHost) : new ProxyHTTP(proxyHost, proxyPort); 243 } 244 245 private static ProxySOCKS5 createProxySOCKS5(final String proxyHost, final int proxyPort) { 246 return proxyPort == 0 ? new ProxySOCKS5(proxyHost) : new ProxySOCKS5(proxyHost, proxyPort); 247 } 248 249 private static Proxy createStreamProxy(final String proxyHost, final int proxyPort, 250 final FileSystemOptions fileSystemOptions, final SftpFileSystemConfigBuilder builder) { 251 // Use a stream proxy, i.e. it will use a remote host as a proxy 252 // and run a command (e.g. netcat) that forwards input/output 253 // to the target host. 254 255 // Here we get the settings for connecting to the proxy: 256 // user, password, options and a command 257 final String proxyUser = builder.getProxyUser(fileSystemOptions); 258 final String proxyPassword = builder.getProxyPassword(fileSystemOptions); 259 final FileSystemOptions proxyOptions = builder.getProxyOptions(fileSystemOptions); 260 261 final String proxyCommand = builder.getProxyCommand(fileSystemOptions); 262 263 // Create the stream proxy 264 return new SftpStreamProxy(proxyCommand, proxyUser, proxyHost, proxyPort, proxyPassword, proxyOptions); 265 } 266 267 /** 268 * Finds the {@code .ssh} directory. 269 * <p> 270 * The lookup order is: 271 * </p> 272 * <ol> 273 * <li>The system property {@code vfs.sftp.sshdir} (the override mechanism)</li> 274 * <li>{@code user.home}/.ssh</li> 275 * <li>On Windows only: {@code C:\cygwin\home[user.name]\.ssh}</li> 276 * <li>The current directory, as a last resort.</li> 277 * </ol> 278 * 279 * <h2>Windows Notes</h2> 280 * <p> 281 * The default installation directory for Cygwin is {@code C:\cygwin}. On my set up (Gary here), I have Cygwin in 282 * {@code C:\bin\cygwin}, not the default. Also, my .ssh directory was created in the {@code user.home} directory. 283 * </p> 284 * 285 * @return The {@code .ssh} directory 286 */ 287 private static File findSshDir() { 288 final String sshDirPath; 289 sshDirPath = System.getProperty("vfs.sftp.sshdir"); 290 if (sshDirPath != null) { 291 final File sshDir = new File(sshDirPath); 292 if (sshDir.exists()) { 293 return sshDir; 294 } 295 } 296 297 File sshDir = new File(SystemProperties.getUserHome(), SSH_DIR_NAME); 298 if (sshDir.exists()) { 299 return sshDir; 300 } 301 302 if (SystemUtils.IS_OS_WINDOWS) { 303 // TODO - this may not be true 304 final String userName = SystemProperties.getUserName(); 305 sshDir = new File("C:\\cygwin\\home\\" + userName + "\\" + SSH_DIR_NAME); 306 if (sshDir.exists()) { 307 return sshDir; 308 } 309 } 310 return new File(""); 311 } 312 313 private static void setConfigRepository(final JSch jsch, final File sshDir, final ConfigRepository configRepository, final boolean loadOpenSSHConfig) 314 throws FileSystemException { 315 if (configRepository != null) { 316 jsch.setConfigRepository(configRepository); 317 } else if (loadOpenSSHConfig) { 318 try { 319 // loading openssh config (~/.ssh/config) 320 final ConfigRepository openSSHConfig = OpenSSHConfig.parseFile(new File(sshDir, OPENSSH_CONFIG_NAME).getAbsolutePath()); 321 jsch.setConfigRepository(openSSHConfig); 322 } catch (final IOException e) { 323 throw new FileSystemException("vfs.provider.sftp/load-openssh-config.error", e); 324 } 325 } 326 } 327 328 private static void setKnownHosts(final JSch jsch, final File sshDir, File knownHostsFile) 329 throws FileSystemException { 330 try { 331 if (knownHostsFile != null) { 332 jsch.setKnownHosts(knownHostsFile.getAbsolutePath()); 333 } else { 334 // Load the known hosts file 335 knownHostsFile = new File(sshDir, "known_hosts"); 336 if (knownHostsFile.isFile() && knownHostsFile.canRead()) { 337 jsch.setKnownHosts(knownHostsFile.getAbsolutePath()); 338 } 339 } 340 } catch (final JSchException e) { 341 throw new FileSystemException("vfs.provider.sftp/known-hosts.error", knownHostsFile.getAbsolutePath(), e); 342 } 343 344 } 345 346 private SftpClientFactory() { 347 } 348}