/*
 * Copyright (C) 2018, Sasa Zivkov <sasa.zivkov@sap.com>
 * Copyright (C) 2016, Mark Ingram <markdingram@gmail.com>
 * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
 * Copyright (C) 2008-2009, Google Inc.
 * Copyright (C) 2009, Google, Inc.
 * Copyright (C) 2009, JetBrains s.r.o.
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.transport;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jcraft.jsch.ConfigRepository;
import com.jcraft.jsch.ConfigRepository.Config;
import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.HostKeyRepository;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

The base session factory that loads known hosts and private keys from $HOME/.ssh.

This is the default implementation used by JGit and provides most of the compatibility necessary to match OpenSSH, a popular implementation of SSH used by C Git.

The factory does not provide UI behavior. Override the method configure(Host, Session) to supply appropriate UserInfo to the session.

/** * The base session factory that loads known hosts and private keys from * <code>$HOME/.ssh</code>. * <p> * This is the default implementation used by JGit and provides most of the * compatibility necessary to match OpenSSH, a popular implementation of SSH * used by C Git. * <p> * The factory does not provide UI behavior. Override the method * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session. */
public abstract class JschConfigSessionFactory extends SshSessionFactory { private static final Logger LOG = LoggerFactory .getLogger(JschConfigSessionFactory.class);
We use different Jsch instances for hosts that have an IdentityFile configured in ~/.ssh/config. Jsch by default would cache decrypted keys only per session, which results in repeated password prompts. Using different Jsch instances, we can cache the keys on these instances so that they will be re-used for successive sessions, and thus the user is prompted for a key password only once while Eclipse runs.
/** * We use different Jsch instances for hosts that have an IdentityFile * configured in ~/.ssh/config. Jsch by default would cache decrypted keys * only per session, which results in repeated password prompts. Using * different Jsch instances, we can cache the keys on these instances so * that they will be re-used for successive sessions, and thus the user is * prompted for a key password only once while Eclipse runs. */
private final Map<String, JSch> byIdentityFile = new HashMap<>(); private JSch defaultJSch; private OpenSshConfig config;
{@inheritDoc}
/** {@inheritDoc} */
@Override public synchronized RemoteSession getSession(URIish uri, CredentialsProvider credentialsProvider, FS fs, int tms) throws TransportException { String user = uri.getUser(); final String pass = uri.getPass(); String host = uri.getHost(); int port = uri.getPort(); try { if (config == null) config = OpenSshConfig.get(fs); final OpenSshConfig.Host hc = config.lookup(host); if (port <= 0) port = hc.getPort(); if (user == null) user = hc.getUser(); Session session = createSession(credentialsProvider, fs, user, pass, host, port, hc); int retries = 0; while (!session.isConnected()) { try { retries++; session.connect(tms); } catch (JSchException e) { session.disconnect(); session = null; // Make sure our known_hosts is not outdated knownHosts(getJSch(hc, fs), fs); if (isAuthenticationCanceled(e)) { throw e; } else if (isAuthenticationFailed(e) && credentialsProvider != null) { // if authentication failed maybe credentials changed at // the remote end therefore reset credentials and retry if (retries < 3) { credentialsProvider.reset(uri); session = createSession(credentialsProvider, fs, user, pass, host, port, hc); } else throw e; } else if (retries >= hc.getConnectionAttempts()) { throw e; } else { try { Thread.sleep(1000); session = createSession(credentialsProvider, fs, user, pass, host, port, hc); } catch (InterruptedException e1) { throw new TransportException( JGitText.get().transportSSHRetryInterrupt, e1); } } } } return new JschSession(session, uri); } catch (JSchException je) { final Throwable c = je.getCause(); if (c instanceof UnknownHostException) { throw new TransportException(uri, JGitText.get().unknownHost, je); } if (c instanceof ConnectException) { throw new TransportException(uri, c.getMessage(), je); } throw new TransportException(uri, je.getMessage(), je); } } private static boolean isAuthenticationFailed(JSchException e) { return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$ } private static boolean isAuthenticationCanceled(JSchException e) { return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$ } // Package visibility for tests Session createSession(CredentialsProvider credentialsProvider, FS fs, String user, final String pass, String host, int port, final OpenSshConfig.Host hc) throws JSchException { final Session session = createSession(hc, user, host, port, fs); // Jsch will have overridden the explicit user by the one from the SSH // config file... setUserName(session, user); // Jsch will also have overridden the port. if (port > 0 && port != session.getPort()) { session.setPort(port); } // We retry already in getSession() method. JSch must not retry // on its own. session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$ if (pass != null) session.setPassword(pass); final String strictHostKeyCheckingPolicy = hc .getStrictHostKeyChecking(); if (strictHostKeyCheckingPolicy != null) session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$ strictHostKeyCheckingPolicy); final String pauth = hc.getPreferredAuthentications(); if (pauth != null) session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$ if (credentialsProvider != null && (!hc.isBatchMode() || !credentialsProvider.isInteractive())) { session.setUserInfo(new CredentialsProviderUserInfo(session, credentialsProvider)); } safeConfig(session, hc.getConfig()); if (hc.getConfig().getValue("HostKeyAlgorithms") == null) { //$NON-NLS-1$ setPreferredKeyTypesOrder(session); } configure(hc, session); return session; } private void safeConfig(Session session, Config cfg) { // Ensure that Jsch checks all configured algorithms, not just its // built-in ones. Otherwise it may propose an algorithm for which it // doesn't have an implementation, and then run into an NPE if that // algorithm ends up being chosen. copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$ copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$ copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$ "CheckSignatures"); //$NON-NLS-1$ } private static void setPreferredKeyTypesOrder(Session session) { HostKeyRepository hkr = session.getHostKeyRepository(); List<String> known = Stream.of(hkr.getHostKey(hostName(session), null)) .map(HostKey::getType) .collect(toList()); if (!known.isEmpty()) { String serverHostKey = "server_host_key"; //$NON-NLS-1$ String current = session.getConfig(serverHostKey); if (current == null) { session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$ return; } String knownFirst = Stream.concat( known.stream(), Stream.of(current.split(",")) //$NON-NLS-1$ .filter(s -> !known.contains(s))) .collect(joining(",")); //$NON-NLS-1$ session.setConfig(serverHostKey, knownFirst); } } private static String hostName(Session s) { if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) { return s.getHost(); } return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$ Integer.valueOf(s.getPort())); } private void copyConfigValueToSession(Session session, Config cfg, String from, String to) { String value = cfg.getValue(from); if (value != null) { session.setConfig(to, value); } } private void setUserName(Session session, String userName) { // Jsch 0.1.54 picks up the user name from the ssh config, even if an // explicit user name was given! We must correct that if ~/.ssh/config // has a different user name. if (userName == null || userName.isEmpty() || userName.equals(session.getUserName())) { return; } try { Class<?>[] parameterTypes = { String.class }; Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$ parameterTypes); method.setAccessible(true); method.invoke(session, userName); } catch (NullPointerException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { LOG.error(MessageFormat.format(JGitText.get().sshUserNameError, userName, session.getUserName()), e); } }
Create a new remote session for the requested address.
Params:
  • hc – host configuration
  • user – login to authenticate as.
  • host – server name to connect to.
  • port – port number of the SSH daemon (typically 22).
  • fs – the file system abstraction which will be necessary to perform certain file system operations.
Throws:
Returns:new session instance, but otherwise unconfigured.
/** * Create a new remote session for the requested address. * * @param hc * host configuration * @param user * login to authenticate as. * @param host * server name to connect to. * @param port * port number of the SSH daemon (typically 22). * @param fs * the file system abstraction which will be necessary to * perform certain file system operations. * @return new session instance, but otherwise unconfigured. * @throws com.jcraft.jsch.JSchException * the session could not be created. */
protected Session createSession(final OpenSshConfig.Host hc, final String user, final String host, final int port, FS fs) throws JSchException { return getJSch(hc, fs).getSession(user, host, port); }
Provide additional configuration for the JSch instance. This method could be overridden to supply a preferred IdentityRepository.
Params:
  • jsch – jsch instance
Since:4.5
/** * Provide additional configuration for the JSch instance. This method could * be overridden to supply a preferred * {@link com.jcraft.jsch.IdentityRepository}. * * @param jsch * jsch instance * @since 4.5 */
protected void configureJSch(JSch jsch) { // No additional configuration required. }
Provide additional configuration for the session based on the host information. This method could be used to supply UserInfo.
Params:
  • hc – host configuration
  • session – session to configure
/** * Provide additional configuration for the session based on the host * information. This method could be used to supply * {@link com.jcraft.jsch.UserInfo}. * * @param hc * host configuration * @param session * session to configure */
protected abstract void configure(OpenSshConfig.Host hc, Session session);
Obtain the JSch used to create new sessions.
Params:
  • hc – host configuration
  • fs – the file system abstraction which will be necessary to perform certain file system operations.
Throws:
Returns:the JSch instance to use.
/** * Obtain the JSch used to create new sessions. * * @param hc * host configuration * @param fs * the file system abstraction which will be necessary to * perform certain file system operations. * @return the JSch instance to use. * @throws com.jcraft.jsch.JSchException * the user configuration could not be created. */
protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException { if (defaultJSch == null) { defaultJSch = createDefaultJSch(fs); if (defaultJSch.getConfigRepository() == null) { defaultJSch.setConfigRepository( new JschBugFixingConfigRepository(config)); } for (Object name : defaultJSch.getIdentityNames()) byIdentityFile.put((String) name, defaultJSch); } final File identityFile = hc.getIdentityFile(); if (identityFile == null) return defaultJSch; final String identityKey = identityFile.getAbsolutePath(); JSch jsch = byIdentityFile.get(identityKey); if (jsch == null) { jsch = new JSch(); configureJSch(jsch); if (jsch.getConfigRepository() == null) { jsch.setConfigRepository(defaultJSch.getConfigRepository()); } jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository()); jsch.addIdentity(identityKey); byIdentityFile.put(identityKey, jsch); } return jsch; }
Create default instance of jsch
Params:
  • fs – the file system abstraction which will be necessary to perform certain file system operations.
Throws:
Returns:the new default JSch implementation.
/** * Create default instance of jsch * * @param fs * the file system abstraction which will be necessary to perform * certain file system operations. * @return the new default JSch implementation. * @throws com.jcraft.jsch.JSchException * known host keys cannot be loaded. */
protected JSch createDefaultJSch(FS fs) throws JSchException { final JSch jsch = new JSch(); JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa")); //$NON-NLS-1$ //$NON-NLS-2$ JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss")); //$NON-NLS-1$ //$NON-NLS-2$ configureJSch(jsch); knownHosts(jsch, fs); identities(jsch, fs); return jsch; } private static void knownHosts(JSch sch, FS fs) throws JSchException { final File home = fs.userHome(); if (home == null) return; final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$ try (FileInputStream in = new FileInputStream(known_hosts)) { sch.setKnownHosts(in); } catch (FileNotFoundException none) { // Oh well. They don't have a known hosts in home. } catch (IOException err) { // Oh well. They don't have a known hosts in home. } } private static void identities(JSch sch, FS fs) { final File home = fs.userHome(); if (home == null) return; final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$ if (sshdir.isDirectory()) { loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$ loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$ loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$ } } private static void loadIdentity(JSch sch, File priv) { if (priv.isFile()) { try { sch.addIdentity(priv.getAbsolutePath()); } catch (JSchException e) { // Instead, pretend the key doesn't exist. } } } private static class JschBugFixingConfigRepository implements ConfigRepository { private final ConfigRepository base; public JschBugFixingConfigRepository(ConfigRepository base) { this.base = base; } @Override public Config getConfig(String host) { return new JschBugFixingConfig(base.getConfig(host)); }
A Config that transforms some values from the config file into the format Jsch 0.1.54 expects. This is a work-around for bugs in Jsch.

Additionally, this config hides the IdentityFile config entries from Jsch; we manage those ourselves. Otherwise Jsch would cache passwords (or rather, decrypted keys) only for a single session, resulting in multiple password prompts for user operations that use several Jsch sessions.

/** * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms * some values from the config file into the format Jsch 0.1.54 expects. * This is a work-around for bugs in Jsch. * <p> * Additionally, this config hides the IdentityFile config entries from * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords * (or rather, decrypted keys) only for a single session, resulting in * multiple password prompts for user operations that use several Jsch * sessions. */
private static class JschBugFixingConfig implements Config { private static final String[] NO_IDENTITIES = {}; private final Config real; public JschBugFixingConfig(Config delegate) { real = delegate; } @Override public String getHostname() { return real.getHostname(); } @Override public String getUser() { return real.getUser(); } @Override public int getPort() { return real.getPort(); } @Override public String getValue(String key) { String k = key.toUpperCase(Locale.ROOT); if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ return null; } String result = real.getValue(key); if (result != null) { if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$ || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$ // These values are in seconds. Jsch 0.1.54 passes them // on as is to java.net.Socket.setSoTimeout(), which // expects milliseconds. So convert here to // milliseconds. try { int timeout = Integer.parseInt(result); result = Long.toString( TimeUnit.SECONDS.toMillis(timeout)); } catch (NumberFormatException e) { // Ignore } } } return result; } @Override public String[] getValues(String key) { String k = key.toUpperCase(Locale.ROOT); if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ return NO_IDENTITIES; } return real.getValues(key); } } }
Set the OpenSshConfig to use. Intended for use in tests.
Params:
  • config – to use
/** * Set the {@link OpenSshConfig} to use. Intended for use in tests. * * @param config * to use */
synchronized void setConfig(OpenSshConfig config) { this.config = config; } }