/* * Copyright (C) 2018, Sasa Zivkov * Copyright (C) 2016, Mark Ingram * Copyright (C) 2009, Constantine Plotnikov * Copyright (C) 2008-2009, Google Inc. * Copyright (C) 2009, Google, Inc. * Copyright (C) 2009, JetBrains s.r.o. * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce * 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 static org.eclipse.jgit.transport.OpenSshConfig.SSH_PORT; 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 * {@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. */ private final Map byIdentityFile = new HashMap<>(); private JSch defaultJSch; private OpenSshConfig config; /** {@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 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() == SSH_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. * * @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 * {@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 * {@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. * * @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 * * @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 {@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. *

* 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 {@link OpenSshConfig} to use. Intended for use in tests. * * @param config * to use */ void setConfig(OpenSshConfig config) { this.config = config; } }