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.Objects;
23  import java.util.Properties;
24  
25  import org.apache.commons.lang3.SystemProperties;
26  import org.apache.commons.lang3.SystemUtils;
27  import org.apache.commons.lang3.time.DurationUtils;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.commons.vfs2.FileSystemException;
31  import org.apache.commons.vfs2.FileSystemOptions;
32  
33  import com.jcraft.jsch.ConfigRepository;
34  import com.jcraft.jsch.JSch;
35  import com.jcraft.jsch.JSchException;
36  import com.jcraft.jsch.Logger;
37  import com.jcraft.jsch.OpenSSHConfig;
38  import com.jcraft.jsch.Proxy;
39  import com.jcraft.jsch.ProxyHTTP;
40  import com.jcraft.jsch.ProxySOCKS5;
41  import com.jcraft.jsch.Session;
42  import com.jcraft.jsch.UserInfo;
43  
44  /**
45   * Create a JSch Session instance.
46   */
47  public final class SftpClientFactory {
48  
49      /** Interface JSchLogger with JCL. */
50      private static final class JSchLogger implements Logger {
51          @Override
52          public boolean isEnabled(final int level) {
53              switch (level) {
54              case FATAL:
55                  return LOG.isFatalEnabled();
56              case ERROR:
57                  return LOG.isErrorEnabled();
58              case WARN:
59                  return LOG.isDebugEnabled();
60              case DEBUG:
61                  return LOG.isDebugEnabled();
62              case INFO:
63                  return LOG.isInfoEnabled();
64              default:
65                  return LOG.isDebugEnabled();
66              }
67          }
68  
69          @Override
70          public void log(final int level, final String msg) {
71              switch (level) {
72              case FATAL:
73                  LOG.fatal(msg);
74                  break;
75              case ERROR:
76                  LOG.error(msg);
77                  break;
78              case WARN:
79                  LOG.warn(msg);
80                  break;
81              case DEBUG:
82                  LOG.debug(msg);
83                  break;
84              case INFO:
85                  LOG.info(msg);
86                  break;
87              default:
88                  LOG.debug(msg);
89              }
90          }
91      }
92      private static final String KEY_COMPRESSION_C2S = "compression.c2s";
93      private static final String KEY_COMPRESSION_S2C = "compression.s2c";
94      private static final String KEY_PREFERRED_AUTHENTICATIONS = "PreferredAuthentications";
95  
96      private static final String KEY_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
97  
98      private static final String SSH_DIR_NAME = ".ssh";
99      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 }