From 9683bc71b6c05ce1ca2b5d6c6f7d74fff3db8429 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Sun, 12 Sep 2021 23:35:33 +0200 Subject: Fix split package in bundle org.eclipse.jgit.ssh.jsch The package org.eclipse.jgit.transport was split between org.eclipse.jgit and org.eclipse.jgit.ssh.jsch. Bug: 564544 Change-Id: I91d38e67c65ed97a880f8dc8f9559663b9eec33b --- .../jgit/internal/transport/jsch/JSchText.java | 35 -- .../ssh/jsch/CredentialsProviderUserInfo.java | 136 +++++ .../jgit/internal/transport/ssh/jsch/JSchText.java | 35 ++ .../transport/CredentialsProviderUserInfo.java | 133 ----- .../jgit/transport/JschConfigSessionFactory.java | 541 -------------------- .../org/eclipse/jgit/transport/JschSession.java | 390 -------------- .../org/eclipse/jgit/transport/OpenSshConfig.java | 346 ------------- .../ssh/jsch/JschConfigSessionFactory.java | 560 +++++++++++++++++++++ .../jgit/transport/ssh/jsch/JschSession.java | 394 +++++++++++++++ .../jgit/transport/ssh/jsch/OpenSshConfig.java | 361 +++++++++++++ 10 files changed, 1486 insertions(+), 1445 deletions(-) delete mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/jsch/JSchText.java create mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/CredentialsProviderUserInfo.java create mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/JSchText.java delete mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java delete mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java delete mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java delete mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/OpenSshConfig.java create mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschConfigSessionFactory.java create mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java create mode 100644 org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/OpenSshConfig.java (limited to 'org.eclipse.jgit.ssh.jsch/src') diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/jsch/JSchText.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/jsch/JSchText.java deleted file mode 100644 index 4d4c9cb250..0000000000 --- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/jsch/JSchText.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2020, Michael Dardis and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.internal.transport.jsch; - -import org.eclipse.jgit.nls.NLS; -import org.eclipse.jgit.nls.TranslationBundle; - -/** - * Externalized text messages for localization. - */ -public final class JSchText extends TranslationBundle { - - /** - * Get an instance of this translation bundle. - * - * @return an instance of this translation bundle - */ - public static JSchText get() { - return NLS.getBundleFor(JSchText.class); - } - - // @formatter:off - /***/ public String connectionFailed; - /***/ public String sshUserNameError; - /***/ public String transportSSHRetryInterrupt; - /***/ public String unknownHost; - -} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/CredentialsProviderUserInfo.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/CredentialsProviderUserInfo.java new file mode 100644 index 0000000000..5053493005 --- /dev/null +++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/CredentialsProviderUserInfo.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.transport.ssh.jsch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UIKeyboardInteractive; +import com.jcraft.jsch.UserInfo; + +/** + * A JSch {@link com.jcraft.jsch.UserInfo} adapter for a + * {@link org.eclipse.jgit.transport.CredentialsProvider}. + */ +public class CredentialsProviderUserInfo implements UserInfo, + UIKeyboardInteractive { + private final URIish uri; + + private final CredentialsProvider provider; + + private String password; + + private String passphrase; + + /** + * Wrap a CredentialsProvider to make it suitable for use with JSch. + * + * @param session + * the JSch session this UserInfo will support authentication on. + * @param credentialsProvider + * the provider that will perform the authentication. + */ + public CredentialsProviderUserInfo(Session session, + CredentialsProvider credentialsProvider) { + this.uri = createURI(session); + this.provider = credentialsProvider; + } + + private static URIish createURI(Session session) { + URIish uri = new URIish(); + uri = uri.setScheme("ssh"); //$NON-NLS-1$ + uri = uri.setUser(session.getUserName()); + uri = uri.setHost(session.getHost()); + uri = uri.setPort(session.getPort()); + return uri; + } + + /** {@inheritDoc} */ + @Override + public String getPassword() { + return password; + } + + /** {@inheritDoc} */ + @Override + public String getPassphrase() { + return passphrase; + } + + /** {@inheritDoc} */ + @Override + public boolean promptPassphrase(String msg) { + CredentialItem.StringType v = newPrompt(msg); + if (provider.get(uri, v)) { + passphrase = v.getValue(); + return true; + } + passphrase = null; + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean promptPassword(String msg) { + CredentialItem.Password p = new CredentialItem.Password(msg); + if (provider.get(uri, p)) { + password = new String(p.getValue()); + return true; + } + password = null; + return false; + } + + private CredentialItem.StringType newPrompt(String msg) { + return new CredentialItem.StringType(msg, true); + } + + /** {@inheritDoc} */ + @Override + public boolean promptYesNo(String msg) { + CredentialItem.YesNoType v = new CredentialItem.YesNoType(msg); + return provider.get(uri, v) && v.getValue(); + } + + /** {@inheritDoc} */ + @Override + public void showMessage(String msg) { + provider.get(uri, new CredentialItem.InformationalMessage(msg)); + } + + /** {@inheritDoc} */ + @Override + public String[] promptKeyboardInteractive(String destination, String name, + String instruction, String[] prompt, boolean[] echo) { + CredentialItem.StringType[] v = new CredentialItem.StringType[prompt.length]; + for (int i = 0; i < prompt.length; i++) + v[i] = new CredentialItem.StringType(prompt[i], !echo[i]); + + List items = new ArrayList<>(); + if (instruction != null && instruction.length() > 0) + items.add(new CredentialItem.InformationalMessage(instruction)); + items.addAll(Arrays.asList(v)); + + if (!provider.get(uri, items)) + return null; // cancel + + String[] result = new String[v.length]; + for (int i = 0; i < v.length; i++) + result[i] = v[i].getValue(); + return result; + } +} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/JSchText.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/JSchText.java new file mode 100644 index 0000000000..c090cd7245 --- /dev/null +++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/internal/transport/ssh/jsch/JSchText.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020, Michael Dardis and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.ssh.jsch; + +import org.eclipse.jgit.nls.NLS; +import org.eclipse.jgit.nls.TranslationBundle; + +/** + * Externalized text messages for localization. + */ +public final class JSchText extends TranslationBundle { + + /** + * Get an instance of this translation bundle. + * + * @return an instance of this translation bundle + */ + public static JSchText get() { + return NLS.getBundleFor(JSchText.class); + } + + // @formatter:off + /***/ public String connectionFailed; + /***/ public String sshUserNameError; + /***/ public String transportSSHRetryInterrupt; + /***/ public String unknownHost; + +} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java deleted file mode 100644 index 01adcf30b0..0000000000 --- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2010, Google Inc. and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 -package org.eclipse.jgit.transport; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UIKeyboardInteractive; -import com.jcraft.jsch.UserInfo; - -/** - * A JSch {@link com.jcraft.jsch.UserInfo} adapter for a - * {@link org.eclipse.jgit.transport.CredentialsProvider}. - */ -public class CredentialsProviderUserInfo implements UserInfo, - UIKeyboardInteractive { - private final URIish uri; - - private final CredentialsProvider provider; - - private String password; - - private String passphrase; - - /** - * Wrap a CredentialsProvider to make it suitable for use with JSch. - * - * @param session - * the JSch session this UserInfo will support authentication on. - * @param credentialsProvider - * the provider that will perform the authentication. - */ - public CredentialsProviderUserInfo(Session session, - CredentialsProvider credentialsProvider) { - this.uri = createURI(session); - this.provider = credentialsProvider; - } - - private static URIish createURI(Session session) { - URIish uri = new URIish(); - uri = uri.setScheme("ssh"); //$NON-NLS-1$ - uri = uri.setUser(session.getUserName()); - uri = uri.setHost(session.getHost()); - uri = uri.setPort(session.getPort()); - return uri; - } - - /** {@inheritDoc} */ - @Override - public String getPassword() { - return password; - } - - /** {@inheritDoc} */ - @Override - public String getPassphrase() { - return passphrase; - } - - /** {@inheritDoc} */ - @Override - public boolean promptPassphrase(String msg) { - CredentialItem.StringType v = newPrompt(msg); - if (provider.get(uri, v)) { - passphrase = v.getValue(); - return true; - } - passphrase = null; - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean promptPassword(String msg) { - CredentialItem.Password p = new CredentialItem.Password(msg); - if (provider.get(uri, p)) { - password = new String(p.getValue()); - return true; - } - password = null; - return false; - } - - private CredentialItem.StringType newPrompt(String msg) { - return new CredentialItem.StringType(msg, true); - } - - /** {@inheritDoc} */ - @Override - public boolean promptYesNo(String msg) { - CredentialItem.YesNoType v = new CredentialItem.YesNoType(msg); - return provider.get(uri, v) && v.getValue(); - } - - /** {@inheritDoc} */ - @Override - public void showMessage(String msg) { - provider.get(uri, new CredentialItem.InformationalMessage(msg)); - } - - /** {@inheritDoc} */ - @Override - public String[] promptKeyboardInteractive(String destination, String name, - String instruction, String[] prompt, boolean[] echo) { - CredentialItem.StringType[] v = new CredentialItem.StringType[prompt.length]; - for (int i = 0; i < prompt.length; i++) - v[i] = new CredentialItem.StringType(prompt[i], !echo[i]); - - List items = new ArrayList<>(); - if (instruction != null && instruction.length() > 0) - items.add(new CredentialItem.InformationalMessage(instruction)); - items.addAll(Arrays.asList(v)); - - if (!provider.get(uri, items)) - return null; // cancel - - String[] result = new String[v.length]; - for (int i = 0; i < v.length; i++) - result[i] = v[i].getValue(); - return result; - } -} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java deleted file mode 100644 index 88202dd2d6..0000000000 --- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * 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 others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 -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.transport.jsch.JSchText; -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 class JschConfigSessionFactory extends SshSessionFactory { - - private static final String JSCH = "jsch"; //$NON-NLS-1$ - - 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( - JSchText.get().transportSSHRetryInterrupt, - e1); - } - } - } - } - - return new JschSession(session, uri); - - } catch (JSchException je) { - final Throwable c = je.getCause(); - if (c instanceof UnknownHostException) { - throw new TransportException(uri, - JSchText.get().unknownHost, - je); - } - if (c instanceof ConnectException) { - throw new TransportException(uri, c.getMessage(), je); - } - throw new TransportException(uri, je.getMessage(), je); - } - - } - - @Override - public String getType() { - return JSCH; - } - - 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(); - HostKey[] hostKeys = hkr.getHostKey(hostName(session), null); - - if (hostKeys == null) { - return; - } - - List known = Stream.of(hostKeys) - .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(JSchText.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 void configure(OpenSshConfig.Host hc, Session session) { - // No additional configuration required. - } - - /** - * 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 - */ - synchronized void setConfig(OpenSshConfig config) { - this.config = config; - } -} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java deleted file mode 100644 index c7d0941b62..0000000000 --- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * 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 others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 -package org.eclipse.jgit.transport; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jgit.errors.TransportException; -import org.eclipse.jgit.internal.transport.jsch.JSchText; -import org.eclipse.jgit.util.io.IsolatedOutputStream; - -import com.jcraft.jsch.Channel; -import com.jcraft.jsch.ChannelExec; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.SftpException; - -/** - * Run remote commands using Jsch. - *

- * This class is the default session implementation using Jsch. Note that - * {@link org.eclipse.jgit.transport.JschConfigSessionFactory} is used to create - * the actual session passed to the constructor. - */ -public class JschSession implements RemoteSession2 { - final Session sock; - final URIish uri; - - /** - * Create a new session object by passing the real Jsch session and the URI - * information. - * - * @param session - * the real Jsch session created elsewhere. - * @param uri - * the URI information for the remote connection - */ - public JschSession(Session session, URIish uri) { - sock = session; - this.uri = uri; - } - - /** {@inheritDoc} */ - @Override - public Process exec(String command, int timeout) throws IOException { - return exec(command, Collections.emptyMap(), timeout); - } - - /** {@inheritDoc} */ - @Override - public Process exec(String command, Map environment, - int timeout) throws IOException { - return new JschProcess(command, environment, timeout); - } - - /** {@inheritDoc} */ - @Override - public void disconnect() { - if (sock.isConnected()) - sock.disconnect(); - } - - /** - * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get - * an Sftp channel from Jsch. Ideally, this method would be generic, which - * would require implementing generic Sftp channel operations in the - * RemoteSession class. - * - * @return a channel suitable for Sftp operations. - * @throws com.jcraft.jsch.JSchException - * on problems getting the channel. - * @deprecated since 5.2; use {@link #getFtpChannel()} instead - */ - @Deprecated - public Channel getSftpChannel() throws JSchException { - return sock.openChannel("sftp"); //$NON-NLS-1$ - } - - /** - * {@inheritDoc} - * - * @since 5.2 - */ - @Override - public FtpChannel getFtpChannel() { - return new JschFtpChannel(); - } - - /** - * Implementation of Process for running a single command using Jsch. - *

- * Uses the Jsch session to do actual command execution and manage the - * execution. - */ - private class JschProcess extends Process { - private ChannelExec channel; - - final int timeout; - - private InputStream inputStream; - - private OutputStream outputStream; - - private InputStream errStream; - - /** - * Opens a channel on the session ("sock") for executing the given - * command, opens streams, and starts command execution. - * - * @param commandName - * the command to execute - * @param environment - * environment variables to pass on - * @param tms - * the timeout value, in seconds, for the command. - * @throws TransportException - * on problems opening a channel or connecting to the remote - * host - * @throws IOException - * on problems opening streams - */ - JschProcess(String commandName, Map environment, - int tms) throws TransportException, IOException { - timeout = tms; - try { - channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$ - if (environment != null) { - for (Map.Entry envVar : environment - .entrySet()) { - channel.setEnv(envVar.getKey(), envVar.getValue()); - } - } - channel.setCommand(commandName); - setupStreams(); - channel.connect(timeout > 0 ? timeout * 1000 : 0); - if (!channel.isConnected()) { - closeOutputStream(); - throw new TransportException(uri, - JSchText.get().connectionFailed); - } - } catch (JSchException e) { - closeOutputStream(); - throw new TransportException(uri, e.getMessage(), e); - } - } - - private void closeOutputStream() { - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException ioe) { - // ignore - } - } - } - - private void setupStreams() throws IOException { - inputStream = channel.getInputStream(); - - // JSch won't let us interrupt writes when we use our InterruptTimer - // to break out of a long-running write operation. To work around - // that we spawn a background thread to shuttle data through a pipe, - // as we can issue an interrupted write out of that. Its slower, so - // we only use this route if there is a timeout. - OutputStream out = channel.getOutputStream(); - if (timeout <= 0) { - outputStream = out; - } else { - IsolatedOutputStream i = new IsolatedOutputStream(out); - outputStream = new BufferedOutputStream(i, 16 * 1024); - } - - errStream = channel.getErrStream(); - } - - @Override - public InputStream getInputStream() { - return inputStream; - } - - @Override - public OutputStream getOutputStream() { - return outputStream; - } - - @Override - public InputStream getErrorStream() { - return errStream; - } - - @Override - public int exitValue() { - if (isRunning()) - throw new IllegalThreadStateException(); - return channel.getExitStatus(); - } - - private boolean isRunning() { - return channel.getExitStatus() < 0 && channel.isConnected(); - } - - @Override - public void destroy() { - if (channel.isConnected()) - channel.disconnect(); - closeOutputStream(); - } - - @Override - public int waitFor() throws InterruptedException { - while (isRunning()) - Thread.sleep(100); - return exitValue(); - } - } - - private class JschFtpChannel implements FtpChannel { - - private ChannelSftp ftp; - - @Override - public void connect(int timeout, TimeUnit unit) throws IOException { - try { - ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ - ftp.connect((int) unit.toMillis(timeout)); - } catch (JSchException e) { - ftp = null; - throw new IOException(e.getLocalizedMessage(), e); - } - } - - @Override - public void disconnect() { - ftp.disconnect(); - ftp = null; - } - - private T map(Callable op) throws IOException { - try { - return op.call(); - } catch (Exception e) { - if (e instanceof SftpException) { - throw new FtpChannel.FtpException(e.getLocalizedMessage(), - ((SftpException) e).id, e); - } - throw new IOException(e.getLocalizedMessage(), e); - } - } - - @Override - public boolean isConnected() { - return ftp != null && sock.isConnected(); - } - - @Override - public void cd(String path) throws IOException { - map(() -> { - ftp.cd(path); - return null; - }); - } - - @Override - public String pwd() throws IOException { - return map(() -> ftp.pwd()); - } - - @Override - public Collection ls(String path) throws IOException { - return map(() -> { - List result = new ArrayList<>(); - for (Object e : ftp.ls(path)) { - ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; - result.add(new DirEntry() { - - @Override - public String getFilename() { - return entry.getFilename(); - } - - @Override - public long getModifiedTime() { - return entry.getAttrs().getMTime(); - } - - @Override - public boolean isDirectory() { - return entry.getAttrs().isDir(); - } - }); - } - return result; - }); - } - - @Override - public void rmdir(String path) throws IOException { - map(() -> { - ftp.rm(path); - return null; - }); - } - - @Override - public void mkdir(String path) throws IOException { - map(() -> { - ftp.mkdir(path); - return null; - }); - } - - @Override - public InputStream get(String path) throws IOException { - return map(() -> ftp.get(path)); - } - - @Override - public OutputStream put(String path) throws IOException { - return map(() -> ftp.put(path)); - } - - @Override - public void rm(String path) throws IOException { - map(() -> { - ftp.rm(path); - return null; - }); - } - - @Override - public void rename(String from, String to) throws IOException { - map(() -> { - // Plain FTP rename will fail if "to" exists. Jsch knows about - // the FTP extension "posix-rename@openssh.com", which will - // remove "to" first if it exists. - if (hasPosixRename()) { - ftp.rename(from, to); - } else if (!to.equals(from)) { - // Try to remove "to" first. With git, we typically get this - // when a lock file is moved over the file locked. Note that - // the check for to being equal to from may still fail in - // the general case, but for use with JGit's TransportSftp - // it should be good enough. - delete(to); - ftp.rename(from, to); - } - return null; - }); - } - - /** - * Determine whether the server has the posix-rename extension. - * - * @return {@code true} if it is supported, {@code false} otherwise - * @see OpenSSH - * deviations and extensions to the published SSH protocol - * @see stdio.h: - * rename() - */ - private boolean hasPosixRename() { - return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ - } - } -} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/OpenSshConfig.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/OpenSshConfig.java deleted file mode 100644 index 5c6c80c768..0000000000 --- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/OpenSshConfig.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2008, 2018, Google Inc. and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 -package org.eclipse.jgit.transport; - -import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive; - -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; -import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry; -import org.eclipse.jgit.util.FS; - -import com.jcraft.jsch.ConfigRepository; - -/** - * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file. - *

- * JSch does have its own config file parser - * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a - * number of problems: - *

    - *
  • it splits lines of the format "keyword = value" wrongly: you'd end up - * with the value "= value". - *
  • its "Host" keyword is not case insensitive. - *
  • it doesn't handle quoted values. - *
  • JSch's OpenSSHConfig doesn't monitor for config file changes. - *
- *

- * This parser makes the critical options available to - * {@link org.eclipse.jgit.transport.SshSessionFactory} via - * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned - * by {@link #lookup(String)}, and implements a fully conforming - * {@link com.jcraft.jsch.ConfigRepository} providing - * {@link com.jcraft.jsch.ConfigRepository.Config}s via - * {@link #getConfig(String)}. - *

- * - * @see OpenSshConfigFile - */ -public class OpenSshConfig implements ConfigRepository { - - /** - * Obtain the user's configuration data. - *

- * The configuration file is always returned to the caller, even if no file - * exists in the user's home directory at the time the call was made. Lookup - * requests are cached and are automatically updated if the user modifies - * the configuration file since the last time it was cached. - * - * @param fs - * the file system abstraction which will be necessary to - * perform certain file system operations. - * @return a caching reader of the user's configuration file. - */ - public static OpenSshConfig get(FS fs) { - File home = fs.userHome(); - if (home == null) - home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ - - final File config = new File(new File(home, SshConstants.SSH_DIR), - SshConstants.CONFIG); - return new OpenSshConfig(home, config); - } - - /** The base file. */ - private OpenSshConfigFile configFile; - - OpenSshConfig(File h, File cfg) { - configFile = new OpenSshConfigFile(h, cfg, - SshSessionFactory.getLocalUserName()); - } - - /** - * Locate the configuration for a specific host request. - * - * @param hostName - * the name the user has supplied to the SSH tool. This may be a - * real host name, or it may just be a "Host" block in the - * configuration file. - * @return r configuration for the requested name. Never null. - */ - public Host lookup(String hostName) { - HostEntry entry = configFile.lookup(hostName, -1, null); - return new Host(entry, hostName, configFile.getLocalUserName()); - } - - /** - * Configuration of one "Host" block in the configuration file. - *

- * If returned from {@link OpenSshConfig#lookup(String)} some or all of the - * properties may not be populated. The properties which are not populated - * should be defaulted by the caller. - *

- * When returned from {@link OpenSshConfig#lookup(String)} any wildcard - * entries which appear later in the configuration file will have been - * already merged into this block. - */ - public static class Host { - String hostName; - - int port; - - File identityFile; - - String user; - - String preferredAuthentications; - - Boolean batchMode; - - String strictHostKeyChecking; - - int connectionAttempts; - - private HostEntry entry; - - private Config config; - - // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys - // to ssh-config keys. - private static final Map KEY_MAP = new TreeMap<>( - String.CASE_INSENSITIVE_ORDER); - - static { - KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$ - KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$ - KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$ - KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$ - KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$ - KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$ - KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$ - KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$ - KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$ - SshConstants.NUMBER_OF_PASSWORD_PROMPTS); - } - - private static String mapKey(String key) { - String k = KEY_MAP.get(key); - return k != null ? k : key; - } - - /** - * Creates a new uninitialized {@link Host}. - */ - public Host() { - // For API backwards compatibility with pre-4.9 JGit - } - - Host(HostEntry entry, String hostName, String localUserName) { - this.entry = entry; - complete(hostName, localUserName); - } - - /** - * @return the value StrictHostKeyChecking property, the valid values - * are "yes" (unknown hosts are not accepted), "no" (unknown - * hosts are always accepted), and "ask" (user should be asked - * before accepting the host) - */ - public String getStrictHostKeyChecking() { - return strictHostKeyChecking; - } - - /** - * @return the real IP address or host name to connect to; never null. - */ - public String getHostName() { - return hostName; - } - - /** - * @return the real port number to connect to; never 0. - */ - public int getPort() { - return port; - } - - /** - * @return path of the private key file to use for authentication; null - * if the caller should use default authentication strategies. - */ - public File getIdentityFile() { - return identityFile; - } - - /** - * @return the real user name to connect as; never null. - */ - public String getUser() { - return user; - } - - /** - * @return the preferred authentication methods, separated by commas if - * more than one authentication method is preferred. - */ - public String getPreferredAuthentications() { - return preferredAuthentications; - } - - /** - * @return true if batch (non-interactive) mode is preferred for this - * host connection. - */ - public boolean isBatchMode() { - return batchMode != null && batchMode.booleanValue(); - } - - /** - * @return the number of tries (one per second) to connect before - * exiting. The argument must be an integer. This may be useful - * in scripts if the connection sometimes fails. The default is - * 1. - * @since 3.4 - */ - public int getConnectionAttempts() { - return connectionAttempts; - } - - - private void complete(String initialHostName, String localUserName) { - // Try to set values from the options. - hostName = entry.getValue(SshConstants.HOST_NAME); - user = entry.getValue(SshConstants.USER); - port = positive(entry.getValue(SshConstants.PORT)); - connectionAttempts = positive( - entry.getValue(SshConstants.CONNECTION_ATTEMPTS)); - strictHostKeyChecking = entry - .getValue(SshConstants.STRICT_HOST_KEY_CHECKING); - batchMode = Boolean.valueOf(OpenSshConfigFile - .flag(entry.getValue(SshConstants.BATCH_MODE))); - preferredAuthentications = entry - .getValue(SshConstants.PREFERRED_AUTHENTICATIONS); - // Fill in defaults if still not set - if (hostName == null || hostName.isEmpty()) { - hostName = initialHostName; - } - if (user == null || user.isEmpty()) { - user = localUserName; - } - if (port <= 0) { - port = SshConstants.SSH_DEFAULT_PORT; - } - if (connectionAttempts <= 0) { - connectionAttempts = 1; - } - List identityFiles = entry - .getValues(SshConstants.IDENTITY_FILE); - if (identityFiles != null && !identityFiles.isEmpty()) { - identityFile = new File(identityFiles.get(0)); - } - } - - Config getConfig() { - if (config == null) { - config = new Config() { - - @Override - public String getHostname() { - return Host.this.getHostName(); - } - - @Override - public String getUser() { - return Host.this.getUser(); - } - - @Override - public int getPort() { - return Host.this.getPort(); - } - - @Override - public String getValue(String key) { - // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() - // for this special case. - if (key.equals("compression.s2c") //$NON-NLS-1$ - || key.equals("compression.c2s")) { //$NON-NLS-1$ - if (!OpenSshConfigFile.flag( - Host.this.entry.getValue(mapKey(key)))) { - return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ - } - return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ - } - return Host.this.entry.getValue(mapKey(key)); - } - - @Override - public String[] getValues(String key) { - List values = Host.this.entry - .getValues(mapKey(key)); - if (values == null) { - return new String[0]; - } - return values.toArray(new String[0]); - } - }; - } - return config; - } - - @Override - @SuppressWarnings("nls") - public String toString() { - return "Host [hostName=" + hostName + ", port=" + port - + ", identityFile=" + identityFile + ", user=" + user - + ", preferredAuthentications=" + preferredAuthentications - + ", batchMode=" + batchMode + ", strictHostKeyChecking=" - + strictHostKeyChecking + ", connectionAttempts=" - + connectionAttempts + ", entry=" + entry + "]"; - } - } - - /** - * {@inheritDoc} - *

- * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config} - * for the given host name. Should be called only by Jsch and tests. - * - * @since 4.9 - */ - @Override - public Config getConfig(String hostName) { - Host host = lookup(hostName); - return host.getConfig(); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$ - } -} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschConfigSessionFactory.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschConfigSessionFactory.java new file mode 100644 index 0000000000..453433e0c4 --- /dev/null +++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschConfigSessionFactory.java @@ -0,0 +1,560 @@ +/* + * 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 others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport.ssh.jsch; + +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.transport.ssh.jsch.CredentialsProviderUserInfo; +import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.RemoteSession; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.URIish; +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.ssh.jsch.OpenSshConfig.Host, Session)} + * to supply appropriate {@link com.jcraft.jsch.UserInfo} to the session. + * + * @since 6.0 + */ +public class JschConfigSessionFactory extends SshSessionFactory { + + private static final String JSCH = "jsch"; //$NON-NLS-1$ + + 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( + JSchText.get().transportSSHRetryInterrupt, + e1); + } + } + } + } + + return new JschSession(session, uri); + + } catch (JSchException je) { + final Throwable c = je.getCause(); + if (c instanceof UnknownHostException) { + throw new TransportException(uri, + JSchText.get().unknownHost, + je); + } + if (c instanceof ConnectException) { + throw new TransportException(uri, c.getMessage(), je); + } + throw new TransportException(uri, je.getMessage(), je); + } + + } + + @Override + public String getType() { + return JSCH; + } + + 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$ + } + + /** + * Use for tests only + * + * @param credentialsProvider + * @param fs + * @param user + * @param pass + * @param host + * @param port + * @param hc + * @return session + * @throws JSchException + */ + public 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(); + HostKey[] hostKeys = hkr.getHostKey(hostName(session), null); + + if (hostKeys == null) { + return; + } + + List known = Stream.of(hostKeys) + .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(JSchText.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 void configure(OpenSshConfig.Host hc, Session session) { + // No additional configuration required. + } + + /** + * 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 + */ + public synchronized void setConfig(OpenSshConfig config) { + this.config = config; + } +} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java new file mode 100644 index 0000000000..02cdf70812 --- /dev/null +++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java @@ -0,0 +1,394 @@ +/* + * 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 others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport.ssh.jsch; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText; +import org.eclipse.jgit.transport.FtpChannel; +import org.eclipse.jgit.transport.RemoteSession2; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.io.IsolatedOutputStream; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpException; + +/** + * Run remote commands using Jsch. + *

+ * This class is the default session implementation using Jsch. Note that + * {@link org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory} is used to + * create the actual session passed to the constructor. + * + * @since 6.0 + */ +public class JschSession implements RemoteSession2 { + final Session sock; + final URIish uri; + + /** + * Create a new session object by passing the real Jsch session and the URI + * information. + * + * @param session + * the real Jsch session created elsewhere. + * @param uri + * the URI information for the remote connection + */ + public JschSession(Session session, URIish uri) { + sock = session; + this.uri = uri; + } + + /** {@inheritDoc} */ + @Override + public Process exec(String command, int timeout) throws IOException { + return exec(command, Collections.emptyMap(), timeout); + } + + /** {@inheritDoc} */ + @Override + public Process exec(String command, Map environment, + int timeout) throws IOException { + return new JschProcess(command, environment, timeout); + } + + /** {@inheritDoc} */ + @Override + public void disconnect() { + if (sock.isConnected()) + sock.disconnect(); + } + + /** + * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get + * an Sftp channel from Jsch. Ideally, this method would be generic, which + * would require implementing generic Sftp channel operations in the + * RemoteSession class. + * + * @return a channel suitable for Sftp operations. + * @throws com.jcraft.jsch.JSchException + * on problems getting the channel. + * @deprecated since 5.2; use {@link #getFtpChannel()} instead + */ + @Deprecated + public Channel getSftpChannel() throws JSchException { + return sock.openChannel("sftp"); //$NON-NLS-1$ + } + + /** + * {@inheritDoc} + * + * @since 5.2 + */ + @Override + public FtpChannel getFtpChannel() { + return new JschFtpChannel(); + } + + /** + * Implementation of Process for running a single command using Jsch. + *

+ * Uses the Jsch session to do actual command execution and manage the + * execution. + */ + private class JschProcess extends Process { + private ChannelExec channel; + + final int timeout; + + private InputStream inputStream; + + private OutputStream outputStream; + + private InputStream errStream; + + /** + * Opens a channel on the session ("sock") for executing the given + * command, opens streams, and starts command execution. + * + * @param commandName + * the command to execute + * @param environment + * environment variables to pass on + * @param tms + * the timeout value, in seconds, for the command. + * @throws TransportException + * on problems opening a channel or connecting to the remote + * host + * @throws IOException + * on problems opening streams + */ + JschProcess(String commandName, Map environment, + int tms) throws TransportException, IOException { + timeout = tms; + try { + channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$ + if (environment != null) { + for (Map.Entry envVar : environment + .entrySet()) { + channel.setEnv(envVar.getKey(), envVar.getValue()); + } + } + channel.setCommand(commandName); + setupStreams(); + channel.connect(timeout > 0 ? timeout * 1000 : 0); + if (!channel.isConnected()) { + closeOutputStream(); + throw new TransportException(uri, + JSchText.get().connectionFailed); + } + } catch (JSchException e) { + closeOutputStream(); + throw new TransportException(uri, e.getMessage(), e); + } + } + + private void closeOutputStream() { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ioe) { + // ignore + } + } + } + + private void setupStreams() throws IOException { + inputStream = channel.getInputStream(); + + // JSch won't let us interrupt writes when we use our InterruptTimer + // to break out of a long-running write operation. To work around + // that we spawn a background thread to shuttle data through a pipe, + // as we can issue an interrupted write out of that. Its slower, so + // we only use this route if there is a timeout. + OutputStream out = channel.getOutputStream(); + if (timeout <= 0) { + outputStream = out; + } else { + IsolatedOutputStream i = new IsolatedOutputStream(out); + outputStream = new BufferedOutputStream(i, 16 * 1024); + } + + errStream = channel.getErrStream(); + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public InputStream getErrorStream() { + return errStream; + } + + @Override + public int exitValue() { + if (isRunning()) + throw new IllegalThreadStateException(); + return channel.getExitStatus(); + } + + private boolean isRunning() { + return channel.getExitStatus() < 0 && channel.isConnected(); + } + + @Override + public void destroy() { + if (channel.isConnected()) + channel.disconnect(); + closeOutputStream(); + } + + @Override + public int waitFor() throws InterruptedException { + while (isRunning()) + Thread.sleep(100); + return exitValue(); + } + } + + private class JschFtpChannel implements FtpChannel { + + private ChannelSftp ftp; + + @Override + public void connect(int timeout, TimeUnit unit) throws IOException { + try { + ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ + ftp.connect((int) unit.toMillis(timeout)); + } catch (JSchException e) { + ftp = null; + throw new IOException(e.getLocalizedMessage(), e); + } + } + + @Override + public void disconnect() { + ftp.disconnect(); + ftp = null; + } + + private T map(Callable op) throws IOException { + try { + return op.call(); + } catch (Exception e) { + if (e instanceof SftpException) { + throw new FtpChannel.FtpException(e.getLocalizedMessage(), + ((SftpException) e).id, e); + } + throw new IOException(e.getLocalizedMessage(), e); + } + } + + @Override + public boolean isConnected() { + return ftp != null && sock.isConnected(); + } + + @Override + public void cd(String path) throws IOException { + map(() -> { + ftp.cd(path); + return null; + }); + } + + @Override + public String pwd() throws IOException { + return map(() -> ftp.pwd()); + } + + @Override + public Collection ls(String path) throws IOException { + return map(() -> { + List result = new ArrayList<>(); + for (Object e : ftp.ls(path)) { + ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; + result.add(new DirEntry() { + + @Override + public String getFilename() { + return entry.getFilename(); + } + + @Override + public long getModifiedTime() { + return entry.getAttrs().getMTime(); + } + + @Override + public boolean isDirectory() { + return entry.getAttrs().isDir(); + } + }); + } + return result; + }); + } + + @Override + public void rmdir(String path) throws IOException { + map(() -> { + ftp.rm(path); + return null; + }); + } + + @Override + public void mkdir(String path) throws IOException { + map(() -> { + ftp.mkdir(path); + return null; + }); + } + + @Override + public InputStream get(String path) throws IOException { + return map(() -> ftp.get(path)); + } + + @Override + public OutputStream put(String path) throws IOException { + return map(() -> ftp.put(path)); + } + + @Override + public void rm(String path) throws IOException { + map(() -> { + ftp.rm(path); + return null; + }); + } + + @Override + public void rename(String from, String to) throws IOException { + map(() -> { + // Plain FTP rename will fail if "to" exists. Jsch knows about + // the FTP extension "posix-rename@openssh.com", which will + // remove "to" first if it exists. + if (hasPosixRename()) { + ftp.rename(from, to); + } else if (!to.equals(from)) { + // Try to remove "to" first. With git, we typically get this + // when a lock file is moved over the file locked. Note that + // the check for to being equal to from may still fail in + // the general case, but for use with JGit's TransportSftp + // it should be good enough. + delete(to); + ftp.rename(from, to); + } + return null; + }); + } + + /** + * Determine whether the server has the posix-rename extension. + * + * @return {@code true} if it is supported, {@code false} otherwise + * @see OpenSSH + * deviations and extensions to the published SSH protocol + * @see stdio.h: + * rename() + */ + private boolean hasPosixRename() { + return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ + } + } +} diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/OpenSshConfig.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/OpenSshConfig.java new file mode 100644 index 0000000000..2873307e07 --- /dev/null +++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/OpenSshConfig.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2008, 2018, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport.ssh.jsch; + +import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.util.FS; + +import com.jcraft.jsch.ConfigRepository; + +/** + * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file. + *

+ * JSch does have its own config file parser + * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a + * number of problems: + *

    + *
  • it splits lines of the format "keyword = value" wrongly: you'd end up + * with the value "= value". + *
  • its "Host" keyword is not case insensitive. + *
  • it doesn't handle quoted values. + *
  • JSch's OpenSSHConfig doesn't monitor for config file changes. + *
+ *

+ * This parser makes the critical options available to + * {@link org.eclipse.jgit.transport.SshSessionFactory} via + * {@link org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig.Host} objects returned + * by {@link #lookup(String)}, and implements a fully conforming + * {@link com.jcraft.jsch.ConfigRepository} providing + * {@link com.jcraft.jsch.ConfigRepository.Config}s via + * {@link #getConfig(String)}. + *

+ * + * @see OpenSshConfigFile + * @since 6.0 + */ +public class OpenSshConfig implements ConfigRepository { + + /** + * Obtain the user's configuration data. + *

+ * The configuration file is always returned to the caller, even if no file + * exists in the user's home directory at the time the call was made. Lookup + * requests are cached and are automatically updated if the user modifies + * the configuration file since the last time it was cached. + * + * @param fs + * the file system abstraction which will be necessary to + * perform certain file system operations. + * @return a caching reader of the user's configuration file. + */ + public static OpenSshConfig get(FS fs) { + File home = fs.userHome(); + if (home == null) + home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ + + final File config = new File(new File(home, SshConstants.SSH_DIR), + SshConstants.CONFIG); + return new OpenSshConfig(home, config); + } + + /** The base file. */ + private OpenSshConfigFile configFile; + + /** + * Create an OpenSshConfig + * + * @param h + * user's home directory + * @param cfg + * ssh configuration file + */ + public OpenSshConfig(File h, File cfg) { + configFile = new OpenSshConfigFile(h, cfg, + SshSessionFactory.getLocalUserName()); + } + + /** + * Locate the configuration for a specific host request. + * + * @param hostName + * the name the user has supplied to the SSH tool. This may be a + * real host name, or it may just be a "Host" block in the + * configuration file. + * @return r configuration for the requested name. Never null. + */ + public Host lookup(String hostName) { + HostEntry entry = configFile.lookup(hostName, -1, null); + return new Host(entry, hostName, configFile.getLocalUserName()); + } + + /** + * Configuration of one "Host" block in the configuration file. + *

+ * If returned from {@link OpenSshConfig#lookup(String)} some or all of the + * properties may not be populated. The properties which are not populated + * should be defaulted by the caller. + *

+ * When returned from {@link OpenSshConfig#lookup(String)} any wildcard + * entries which appear later in the configuration file will have been + * already merged into this block. + */ + public static class Host { + String hostName; + + int port; + + File identityFile; + + String user; + + String preferredAuthentications; + + Boolean batchMode; + + String strictHostKeyChecking; + + int connectionAttempts; + + private HostEntry entry; + + private Config config; + + // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys + // to ssh-config keys. + private static final Map KEY_MAP = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + + static { + KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$ + KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$ + KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$ + KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$ + KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$ + KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$ + KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$ + KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$ + KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ + KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$ + SshConstants.NUMBER_OF_PASSWORD_PROMPTS); + } + + private static String mapKey(String key) { + String k = KEY_MAP.get(key); + return k != null ? k : key; + } + + /** + * Creates a new uninitialized {@link Host}. + */ + public Host() { + // For API backwards compatibility with pre-4.9 JGit + } + + Host(HostEntry entry, String hostName, String localUserName) { + this.entry = entry; + complete(hostName, localUserName); + } + + /** + * @return the value StrictHostKeyChecking property, the valid values + * are "yes" (unknown hosts are not accepted), "no" (unknown + * hosts are always accepted), and "ask" (user should be asked + * before accepting the host) + */ + public String getStrictHostKeyChecking() { + return strictHostKeyChecking; + } + + /** + * @return the real IP address or host name to connect to; never null. + */ + public String getHostName() { + return hostName; + } + + /** + * @return the real port number to connect to; never 0. + */ + public int getPort() { + return port; + } + + /** + * @return path of the private key file to use for authentication; null + * if the caller should use default authentication strategies. + */ + public File getIdentityFile() { + return identityFile; + } + + /** + * @return the real user name to connect as; never null. + */ + public String getUser() { + return user; + } + + /** + * @return the preferred authentication methods, separated by commas if + * more than one authentication method is preferred. + */ + public String getPreferredAuthentications() { + return preferredAuthentications; + } + + /** + * @return true if batch (non-interactive) mode is preferred for this + * host connection. + */ + public boolean isBatchMode() { + return batchMode != null && batchMode.booleanValue(); + } + + /** + * @return the number of tries (one per second) to connect before + * exiting. The argument must be an integer. This may be useful + * in scripts if the connection sometimes fails. The default is + * 1. + * @since 3.4 + */ + public int getConnectionAttempts() { + return connectionAttempts; + } + + + private void complete(String initialHostName, String localUserName) { + // Try to set values from the options. + hostName = entry.getValue(SshConstants.HOST_NAME); + user = entry.getValue(SshConstants.USER); + port = positive(entry.getValue(SshConstants.PORT)); + connectionAttempts = positive( + entry.getValue(SshConstants.CONNECTION_ATTEMPTS)); + strictHostKeyChecking = entry + .getValue(SshConstants.STRICT_HOST_KEY_CHECKING); + batchMode = Boolean.valueOf(OpenSshConfigFile + .flag(entry.getValue(SshConstants.BATCH_MODE))); + preferredAuthentications = entry + .getValue(SshConstants.PREFERRED_AUTHENTICATIONS); + // Fill in defaults if still not set + if (hostName == null || hostName.isEmpty()) { + hostName = initialHostName; + } + if (user == null || user.isEmpty()) { + user = localUserName; + } + if (port <= 0) { + port = SshConstants.SSH_DEFAULT_PORT; + } + if (connectionAttempts <= 0) { + connectionAttempts = 1; + } + List identityFiles = entry + .getValues(SshConstants.IDENTITY_FILE); + if (identityFiles != null && !identityFiles.isEmpty()) { + identityFile = new File(identityFiles.get(0)); + } + } + + /** + * Get the ssh configuration + * + * @return the ssh configuration + */ + public Config getConfig() { + if (config == null) { + config = new Config() { + + @Override + public String getHostname() { + return Host.this.getHostName(); + } + + @Override + public String getUser() { + return Host.this.getUser(); + } + + @Override + public int getPort() { + return Host.this.getPort(); + } + + @Override + public String getValue(String key) { + // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() + // for this special case. + if (key.equals("compression.s2c") //$NON-NLS-1$ + || key.equals("compression.c2s")) { //$NON-NLS-1$ + if (!OpenSshConfigFile.flag( + Host.this.entry.getValue(mapKey(key)))) { + return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ + } + return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ + } + return Host.this.entry.getValue(mapKey(key)); + } + + @Override + public String[] getValues(String key) { + List values = Host.this.entry + .getValues(mapKey(key)); + if (values == null) { + return new String[0]; + } + return values.toArray(new String[0]); + } + }; + } + return config; + } + + @Override + @SuppressWarnings("nls") + public String toString() { + return "Host [hostName=" + hostName + ", port=" + port + + ", identityFile=" + identityFile + ", user=" + user + + ", preferredAuthentications=" + preferredAuthentications + + ", batchMode=" + batchMode + ", strictHostKeyChecking=" + + strictHostKeyChecking + ", connectionAttempts=" + + connectionAttempts + ", entry=" + entry + "]"; + } + } + + /** + * {@inheritDoc} + *

+ * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config} + * for the given host name. Should be called only by Jsch and tests. + * + * @since 4.9 + */ + @Override + public Config getConfig(String hostName) { + Host host = lookup(hostName); + return host.getConfig(); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$ + } +} -- cgit v1.2.3