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.util.Properties;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.apache.commons.vfs2.FileSystemException;
025import org.apache.commons.vfs2.FileSystemOptions;
026import org.apache.commons.vfs2.util.Os;
027
028import com.jcraft.jsch.JSch;
029import com.jcraft.jsch.JSchException;
030import com.jcraft.jsch.Logger;
031import com.jcraft.jsch.Proxy;
032import com.jcraft.jsch.ProxyHTTP;
033import com.jcraft.jsch.ProxySOCKS5;
034import com.jcraft.jsch.Session;
035import com.jcraft.jsch.UserInfo;
036
037/**
038 * Create a JSch Session instance.
039 */
040public final class SftpClientFactory {
041    private static final String SSH_DIR_NAME = ".ssh";
042
043    private static final Log LOG = LogFactory.getLog(SftpClientFactory.class);
044
045    static {
046        JSch.setLogger(new JSchLogger());
047    }
048
049    private SftpClientFactory() {
050    }
051
052    /**
053     * Creates a new connection to the server.
054     *
055     * @param hostname The name of the host to connect to.
056     * @param port The port to use.
057     * @param username The user's id.
058     * @param password The user's password.
059     * @param fileSystemOptions The FileSystem options.
060     * @return A Session.
061     * @throws FileSystemException if an error occurs.
062     */
063    public static Session createConnection(final String hostname, final int port, final char[] username,
064            final char[] password, final FileSystemOptions fileSystemOptions) throws FileSystemException {
065        final JSch jsch = new JSch();
066
067        File sshDir = null;
068
069        // new style - user passed
070        final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance();
071        final File knownHostsFile = builder.getKnownHosts(fileSystemOptions);
072        final IdentityInfo[] identities = builder.getIdentityInfo(fileSystemOptions);
073        final IdentityRepositoryFactory repositoryFactory = builder.getIdentityRepositoryFactory(fileSystemOptions);
074
075        sshDir = findSshDir();
076
077        setKnownHosts(jsch, sshDir, knownHostsFile);
078
079        if (repositoryFactory != null) {
080            jsch.setIdentityRepository(repositoryFactory.create(jsch));
081        }
082
083        addIdentities(jsch, sshDir, identities);
084
085        Session session;
086        try {
087            session = jsch.getSession(new String(username), hostname, port);
088            if (password != null) {
089                session.setPassword(new String(password));
090            }
091
092            final Integer timeout = builder.getTimeout(fileSystemOptions);
093            if (timeout != null) {
094                session.setTimeout(timeout.intValue());
095            }
096
097            final UserInfo userInfo = builder.getUserInfo(fileSystemOptions);
098            if (userInfo != null) {
099                session.setUserInfo(userInfo);
100            }
101
102            final Properties config = new Properties();
103
104            // set StrictHostKeyChecking property
105            final String strictHostKeyChecking = builder.getStrictHostKeyChecking(fileSystemOptions);
106            if (strictHostKeyChecking != null) {
107                config.setProperty("StrictHostKeyChecking", strictHostKeyChecking);
108            }
109            // set PreferredAuthentications property
110            final String preferredAuthentications = builder.getPreferredAuthentications(fileSystemOptions);
111            if (preferredAuthentications != null) {
112                config.setProperty("PreferredAuthentications", preferredAuthentications);
113            }
114
115            // set compression property
116            final String compression = builder.getCompression(fileSystemOptions);
117            if (compression != null) {
118                config.setProperty("compression.s2c", compression);
119                config.setProperty("compression.c2s", compression);
120            }
121
122            final String proxyHost = builder.getProxyHost(fileSystemOptions);
123            if (proxyHost != null) {
124                final int proxyPort = builder.getProxyPort(fileSystemOptions);
125                final SftpFileSystemConfigBuilder.ProxyType proxyType = builder.getProxyType(fileSystemOptions);
126                Proxy proxy = null;
127                if (SftpFileSystemConfigBuilder.PROXY_HTTP.equals(proxyType)) {
128                    proxy = createProxyHTTP(proxyHost, proxyPort);
129                } else if (SftpFileSystemConfigBuilder.PROXY_SOCKS5.equals(proxyType)) {
130                    proxy = createProxySOCKS5(proxyHost, proxyPort);
131                } else if (SftpFileSystemConfigBuilder.PROXY_STREAM.equals(proxyType)) {
132                    proxy = createStreamProxy(proxyHost, proxyPort, fileSystemOptions, builder);
133                }
134
135                if (proxy != null) {
136                    session.setProxy(proxy);
137                }
138            }
139
140            // set properties for the session
141            if (config.size() > 0) {
142                session.setConfig(config);
143            }
144            session.setDaemonThread(true);
145            session.connect();
146        } catch (final Exception exc) {
147            throw new FileSystemException("vfs.provider.sftp/connect.error", exc, hostname);
148        }
149
150        return session;
151    }
152
153    private static void addIdentities(final JSch jsch, final File sshDir, final IdentityInfo[] identities)
154            throws FileSystemException {
155        if (identities != null) {
156            for (final IdentityInfo info : identities) {
157                addIndentity(jsch, info);
158            }
159        } else {
160            // Load the private key (rsa-key only)
161            final File privateKeyFile = new File(sshDir, "id_rsa");
162            if (privateKeyFile.isFile() && privateKeyFile.canRead()) {
163                addIndentity(jsch, new IdentityInfo(privateKeyFile));
164            }
165        }
166    }
167
168    private static void addIndentity(final JSch jsch, final IdentityInfo info) throws FileSystemException {
169        try {
170            final String privateKeyFile = info.getPrivateKey() != null ? info.getPrivateKey().getAbsolutePath() : null;
171            final String publicKeyFile = info.getPublicKey() != null ? info.getPublicKey().getAbsolutePath() : null;
172            jsch.addIdentity(privateKeyFile, publicKeyFile, info.getPassPhrase());
173        } catch (final JSchException e) {
174            throw new FileSystemException("vfs.provider.sftp/load-private-key.error", info, e);
175        }
176    }
177
178    private static void setKnownHosts(final JSch jsch, final File sshDir, File knownHostsFile)
179            throws FileSystemException {
180        try {
181            if (knownHostsFile != null) {
182                jsch.setKnownHosts(knownHostsFile.getAbsolutePath());
183            } else {
184                // Load the known hosts file
185                knownHostsFile = new File(sshDir, "known_hosts");
186                if (knownHostsFile.isFile() && knownHostsFile.canRead()) {
187                    jsch.setKnownHosts(knownHostsFile.getAbsolutePath());
188                }
189            }
190        } catch (final JSchException e) {
191            throw new FileSystemException("vfs.provider.sftp/known-hosts.error", knownHostsFile.getAbsolutePath(), e);
192        }
193
194    }
195
196    private static Proxy createStreamProxy(final String proxyHost, final int proxyPort,
197            final FileSystemOptions fileSystemOptions, final SftpFileSystemConfigBuilder builder) {
198        Proxy proxy;
199        // Use a stream proxy, i.e. it will use a remote host as a proxy
200        // and run a command (e.g. netcat) that forwards input/output
201        // to the target host.
202
203        // Here we get the settings for connecting to the proxy:
204        // user, password, options and a command
205        final String proxyUser = builder.getProxyUser(fileSystemOptions);
206        final String proxyPassword = builder.getProxyPassword(fileSystemOptions);
207        final FileSystemOptions proxyOptions = builder.getProxyOptions(fileSystemOptions);
208
209        final String proxyCommand = builder.getProxyCommand(fileSystemOptions);
210
211        // Create the stream proxy
212        proxy = new SftpStreamProxy(proxyCommand, proxyUser, proxyHost, proxyPort, proxyPassword, proxyOptions);
213        return proxy;
214    }
215
216    private static ProxySOCKS5 createProxySOCKS5(final String proxyHost, final int proxyPort) {
217        return proxyPort == 0 ? new ProxySOCKS5(proxyHost) : new ProxySOCKS5(proxyHost, proxyPort);
218    }
219
220    private static ProxyHTTP createProxyHTTP(final String proxyHost, final int proxyPort) {
221        return proxyPort == 0 ? new ProxyHTTP(proxyHost) : new ProxyHTTP(proxyHost, proxyPort);
222    }
223
224    /**
225     * Finds the .ssh directory.
226     * <p>
227     * The lookup order is:
228     * <ol>
229     * <li>The system property {@code vfs.sftp.sshdir} (the override mechanism)</li>
230     * <li>{user.home}/.ssh</li>
231     * <li>On Windows only: C:\cygwin\home\{user.name}\.ssh</li>
232     * <li>The current directory, as a last resort.</li>
233     * <ol>
234     * Windows Notes:<br>
235     * The default installation directory for Cygwin is {@code C:\cygwin}. On my set up (Gary here), I have Cygwin in
236     * C:\bin\cygwin, not the default. Also, my .ssh directory was created in the {user.home} directory.
237     *
238     * @return The .ssh directory
239     */
240    private static File findSshDir() {
241        String sshDirPath;
242        sshDirPath = System.getProperty("vfs.sftp.sshdir");
243        if (sshDirPath != null) {
244            final File sshDir = new File(sshDirPath);
245            if (sshDir.exists()) {
246                return sshDir;
247            }
248        }
249
250        File sshDir = new File(System.getProperty("user.home"), SSH_DIR_NAME);
251        if (sshDir.exists()) {
252            return sshDir;
253        }
254
255        if (Os.isFamily(Os.OS_FAMILY_WINDOWS)) {
256            // TODO - this may not be true
257            final String userName = System.getProperty("user.name");
258            sshDir = new File("C:\\cygwin\\home\\" + userName + "\\" + SSH_DIR_NAME);
259            if (sshDir.exists()) {
260                return sshDir;
261            }
262        }
263        return new File("");
264    }
265
266    /** Interface JSchLogger with JCL. */
267    private static class JSchLogger implements Logger {
268        @Override
269        public boolean isEnabled(final int level) {
270            switch (level) {
271            case FATAL:
272                return LOG.isFatalEnabled();
273            case ERROR:
274                return LOG.isErrorEnabled();
275            case WARN:
276                return LOG.isDebugEnabled();
277            case DEBUG:
278                return LOG.isDebugEnabled();
279            case INFO:
280                return LOG.isInfoEnabled();
281            default:
282                return LOG.isDebugEnabled();
283
284            }
285        }
286
287        @Override
288        public void log(final int level, final String msg) {
289            switch (level) {
290            case FATAL:
291                LOG.fatal(msg);
292                break;
293            case ERROR:
294                LOG.error(msg);
295                break;
296            case WARN:
297                LOG.warn(msg);
298                break;
299            case DEBUG:
300                LOG.debug(msg);
301                break;
302            case INFO:
303                LOG.info(msg);
304                break;
305            default:
306                LOG.debug(msg);
307            }
308        }
309    }
310}