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}