View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider.sftp;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.time.Duration;
22  import java.util.Properties;
23  
24  import org.apache.commons.lang3.SystemUtils;
25  import org.apache.commons.lang3.time.DurationUtils;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.commons.vfs2.FileSystemException;
29  import org.apache.commons.vfs2.FileSystemOptions;
30  
31  import com.jcraft.jsch.ConfigRepository;
32  import com.jcraft.jsch.JSch;
33  import com.jcraft.jsch.JSchException;
34  import com.jcraft.jsch.Logger;
35  import com.jcraft.jsch.OpenSSHConfig;
36  import com.jcraft.jsch.Proxy;
37  import com.jcraft.jsch.ProxyHTTP;
38  import com.jcraft.jsch.ProxySOCKS5;
39  import com.jcraft.jsch.Session;
40  import com.jcraft.jsch.UserInfo;
41  
42  /**
43   * Create a JSch Session instance.
44   */
45  public final class SftpClientFactory {
46  
47      /** Interface JSchLogger with JCL. */
48      private static class JSchLogger implements Logger {
49          @Override
50          public boolean isEnabled(final int level) {
51              switch (level) {
52              case FATAL:
53                  return LOG.isFatalEnabled();
54              case ERROR:
55                  return LOG.isErrorEnabled();
56              case WARN:
57                  return LOG.isDebugEnabled();
58              case DEBUG:
59                  return LOG.isDebugEnabled();
60              case INFO:
61                  return LOG.isInfoEnabled();
62              default:
63                  return LOG.isDebugEnabled();
64  
65              }
66          }
67  
68          @Override
69          public void log(final int level, final String msg) {
70              switch (level) {
71              case FATAL:
72                  LOG.fatal(msg);
73                  break;
74              case ERROR:
75                  LOG.error(msg);
76                  break;
77              case WARN:
78                  LOG.warn(msg);
79                  break;
80              case DEBUG:
81                  LOG.debug(msg);
82                  break;
83              case INFO:
84                  LOG.info(msg);
85                  break;
86              default:
87                  LOG.debug(msg);
88              }
89          }
90      }
91      private static final String SSH_DIR_NAME = ".ssh";
92      private static final String OPENSSH_CONFIG_NAME = "config";
93  
94      private static final Log LOG = LogFactory.getLog(SftpClientFactory.class);
95  
96      static {
97          JSch.setLogger(new JSchLogger());
98      }
99  
100     private static void addIdentities(final JSch jsch, final File sshDir, final IdentityProvider[] identities)
101             throws FileSystemException {
102         if (identities != null) {
103             for (final IdentityProvider info : identities) {
104                 addIdentity(jsch, info);
105             }
106         } else {
107             // Load the private key (rsa-key only)
108             final File privateKeyFile = new File(sshDir, "id_rsa");
109             if (privateKeyFile.isFile() && privateKeyFile.canRead()) {
110                 addIdentity(jsch, new IdentityInfo(privateKeyFile));
111             }
112         }
113     }
114 
115     private static void addIdentity(final JSch jsch, final IdentityProvider identity) throws FileSystemException {
116         try {
117             identity.addIdentity(jsch);
118         } catch (final JSchException e) {
119             throw new FileSystemException("vfs.provider.sftp/load-private-key.error", identity, e);
120         }
121     }
122 
123     /**
124      * Creates a new connection to the server.
125      *
126      * @param hostname The name of the host to connect to.
127      * @param port The port to use.
128      * @param username The user's id.
129      * @param password The user's password.
130      * @param fileSystemOptions The FileSystem options.
131      * @return A Session, never null.
132      * @throws FileSystemException if an error occurs.
133      */
134     public static Session createConnection(final String hostname, final int port, final char[] username,
135             final char[] password, final FileSystemOptions fileSystemOptions) throws FileSystemException {
136         final JSch jsch = new JSch();
137 
138         // new style - user passed
139         final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance();
140         final File knownHostsFile = builder.getKnownHosts(fileSystemOptions);
141         final IdentityProvider[] identities = builder.getIdentityProvider(fileSystemOptions);
142         final IdentityRepositoryFactory repositoryFactory = builder.getIdentityRepositoryFactory(fileSystemOptions);
143         final ConfigRepository configRepository = builder.getConfigRepository(fileSystemOptions);
144         final boolean loadOpenSSHConfig = builder.isLoadOpenSSHConfig(fileSystemOptions);
145 
146         final File sshDir = findSshDir();
147 
148         setKnownHosts(jsch, sshDir, knownHostsFile);
149 
150         if (repositoryFactory != null) {
151             jsch.setIdentityRepository(repositoryFactory.create(jsch));
152         }
153 
154         addIdentities(jsch, sshDir, identities);
155         setConfigRepository(jsch, sshDir, configRepository, loadOpenSSHConfig);
156 
157         final Session session;
158         try {
159             session = jsch.getSession(new String(username), hostname, port);
160             if (password != null) {
161                 session.setPassword(new String(password));
162             }
163 
164             final Duration sessionTimeout = builder.getSessionTimeout(fileSystemOptions);
165             if (sessionTimeout != null) {
166                 session.setTimeout(DurationUtils.toMillisInt(sessionTimeout));
167             }
168 
169             final UserInfo userInfo = builder.getUserInfo(fileSystemOptions);
170             if (userInfo != null) {
171                 session.setUserInfo(userInfo);
172             }
173 
174             final Properties config = new Properties();
175 
176             // set StrictHostKeyChecking property
177             final String strictHostKeyChecking = builder.getStrictHostKeyChecking(fileSystemOptions);
178             if (strictHostKeyChecking != null) {
179                 config.setProperty("StrictHostKeyChecking", strictHostKeyChecking);
180             }
181             // set PreferredAuthentications property
182             final String preferredAuthentications = builder.getPreferredAuthentications(fileSystemOptions);
183             if (preferredAuthentications != null) {
184                 config.setProperty("PreferredAuthentications", preferredAuthentications);
185             }
186 
187             // set compression property
188             final String compression = builder.getCompression(fileSystemOptions);
189             if (compression != null) {
190                 config.setProperty("compression.s2c", compression);
191                 config.setProperty("compression.c2s", compression);
192             }
193 
194             final String keyExchangeAlgorithm = builder.getKeyExchangeAlgorithm(fileSystemOptions);
195             if (keyExchangeAlgorithm != null) {
196                 config.setProperty("kex", keyExchangeAlgorithm);
197             }
198 
199             final String proxyHost = builder.getProxyHost(fileSystemOptions);
200             if (proxyHost != null) {
201                 final int proxyPort = builder.getProxyPort(fileSystemOptions);
202                 final SftpFileSystemConfigBuilder.ProxyType proxyType = builder.getProxyType(fileSystemOptions);
203                 final String proxyUser =  builder.getProxyUser(fileSystemOptions);
204                 final String proxyPassword = builder.getProxyPassword(fileSystemOptions);
205                 Proxy proxy = null;
206                 if (SftpFileSystemConfigBuilder.PROXY_HTTP.equals(proxyType)) {
207                     proxy = createProxyHTTP(proxyHost, proxyPort);
208                     ((ProxyHTTP)proxy).setUserPasswd(proxyUser, proxyPassword);
209                 } else if (SftpFileSystemConfigBuilder.PROXY_SOCKS5.equals(proxyType)) {
210                     proxy = createProxySOCKS5(proxyHost, proxyPort);
211                     ((ProxySOCKS5)proxy).setUserPasswd(proxyUser, proxyPassword);
212                 } else if (SftpFileSystemConfigBuilder.PROXY_STREAM.equals(proxyType)) {
213                     proxy = createStreamProxy(proxyHost, proxyPort, fileSystemOptions, builder);
214                 }
215 
216                 if (proxy != null) {
217                     session.setProxy(proxy);
218                 }
219             }
220 
221             // set properties for the session
222             if (!config.isEmpty()) {
223                 session.setConfig(config);
224             }
225             session.setDaemonThread(true);
226             session.connect();
227         } catch (final Exception exc) {
228             throw new FileSystemException("vfs.provider.sftp/connect.error", exc, hostname);
229         }
230 
231         return session;
232     }
233 
234     private static ProxyHTTP createProxyHTTP(final String proxyHost, final int proxyPort) {
235         return proxyPort == 0 ? new ProxyHTTP(proxyHost) : new ProxyHTTP(proxyHost, proxyPort);
236     }
237 
238     private static ProxySOCKS5 createProxySOCKS5(final String proxyHost, final int proxyPort) {
239         return proxyPort == 0 ? new ProxySOCKS5(proxyHost) : new ProxySOCKS5(proxyHost, proxyPort);
240     }
241 
242     private static Proxy createStreamProxy(final String proxyHost, final int proxyPort,
243             final FileSystemOptions fileSystemOptions, final SftpFileSystemConfigBuilder builder) {
244         // Use a stream proxy, i.e. it will use a remote host as a proxy
245         // and run a command (e.g. netcat) that forwards input/output
246         // to the target host.
247 
248         // Here we get the settings for connecting to the proxy:
249         // user, password, options and a command
250         final String proxyUser = builder.getProxyUser(fileSystemOptions);
251         final String proxyPassword = builder.getProxyPassword(fileSystemOptions);
252         final FileSystemOptions proxyOptions = builder.getProxyOptions(fileSystemOptions);
253 
254         final String proxyCommand = builder.getProxyCommand(fileSystemOptions);
255 
256         // Create the stream proxy
257         return new SftpStreamProxy(proxyCommand, proxyUser, proxyHost, proxyPort, proxyPassword, proxyOptions);
258     }
259 
260     /**
261      * Finds the {@code .ssh} directory.
262      * <p>
263      * The lookup order is:
264      * </p>
265      * <ol>
266      * <li>The system property {@code vfs.sftp.sshdir} (the override mechanism)</li>
267      * <li>{@code user.home}/.ssh</li>
268      * <li>On Windows only: {@code C:\cygwin\home[user.name]\.ssh}</li>
269      * <li>The current directory, as a last resort.</li>
270      * </ol>
271      *
272      * <h2>Windows Notes</h2>
273      * <p>
274      * The default installation directory for Cygwin is {@code C:\cygwin}. On my set up (Gary here), I have Cygwin in
275      * {@code C:\bin\cygwin}, not the default. Also, my .ssh directory was created in the {@code user.home} directory.
276      * </p>
277      *
278      * @return The {@code .ssh} directory
279      */
280     private static File findSshDir() {
281         final String sshDirPath;
282         sshDirPath = System.getProperty("vfs.sftp.sshdir");
283         if (sshDirPath != null) {
284             final File sshDir = new File(sshDirPath);
285             if (sshDir.exists()) {
286                 return sshDir;
287             }
288         }
289 
290         File sshDir = new File(System.getProperty("user.home"), SSH_DIR_NAME);
291         if (sshDir.exists()) {
292             return sshDir;
293         }
294 
295         if (SystemUtils.IS_OS_WINDOWS) {
296             // TODO - this may not be true
297             final String userName = System.getProperty("user.name");
298             sshDir = new File("C:\\cygwin\\home\\" + userName + "\\" + SSH_DIR_NAME);
299             if (sshDir.exists()) {
300                 return sshDir;
301             }
302         }
303         return new File("");
304     }
305 
306     private static void setConfigRepository(final JSch jsch, final File sshDir, final ConfigRepository configRepository, final boolean loadOpenSSHConfig) throws FileSystemException {
307         if (configRepository != null) {
308             jsch.setConfigRepository(configRepository);
309         } else if (loadOpenSSHConfig) {
310             try {
311                 // loading openssh config (~/.ssh/config)
312                 final ConfigRepository openSSHConfig = OpenSSHConfig.parseFile(new File(sshDir, OPENSSH_CONFIG_NAME).getAbsolutePath());
313                 jsch.setConfigRepository(openSSHConfig);
314             } catch (final IOException e) {
315                 throw new FileSystemException("vfs.provider.sftp/load-openssh-config.error", e);
316             }
317         }
318     }
319 
320     private static void setKnownHosts(final JSch jsch, final File sshDir, File knownHostsFile)
321             throws FileSystemException {
322         try {
323             if (knownHostsFile != null) {
324                 jsch.setKnownHosts(knownHostsFile.getAbsolutePath());
325             } else {
326                 // Load the known hosts file
327                 knownHostsFile = new File(sshDir, "known_hosts");
328                 if (knownHostsFile.isFile() && knownHostsFile.canRead()) {
329                     jsch.setKnownHosts(knownHostsFile.getAbsolutePath());
330                 }
331             }
332         } catch (final JSchException e) {
333             throw new FileSystemException("vfs.provider.sftp/known-hosts.error", knownHostsFile.getAbsolutePath(), e);
334         }
335 
336     }
337 
338     private SftpClientFactory() {
339     }
340 }