Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Wolf2018-10-20 14:47:52 +0000
committerThomas Wolf2018-11-17 17:57:51 +0000
commit33cc25fcead0ed86bd61c0f87625aac1dcaf6b90 (patch)
treec9fa7bece549ab658933d1af2c25f25a5550a152 /org.eclipse.egit.core
parent2f75c631e5f4e5f0354d18ff0311abf69c6db8bc (diff)
downloadegit-33cc25fcead0ed86bd61c0f87625aac1dcaf6b90.tar.gz
egit-33cc25fcead0ed86bd61c0f87625aac1dcaf6b90.tar.xz
egit-33cc25fcead0ed86bd61c0f87625aac1dcaf6b90.zip
Include the Apache MINA ssh client
Add a preference with UI in the main Git preference page to select between JSch and the new Apache MINA sshd client. Read preferences from the org.eclipse.jsch.core preference node and install a listener to pick up preference changes. The session factory thus can use the ssh preferences as configured by the user. For the time being, we use the ssh directory, the default identities, and the preferred authentication mechanisms. The latter two can be overridden through the ssh config file, which will be the one in the configured ssh directory. The other preferences from org.eclipse.jsch are not taken into account; sshd may have different algorithms available and I don't want to change preferences in a foreign bundle. Make the proxy service accessible in the EGit core activator, and use it in a ProxyDataFactory in the EGitSshdSessionFactory. The EGitSshdSessionFactory has no key cache. Eclipse is a long-running application, and keeping ssh keys in memory for that long is most probably not wise. Instead re-load keys whenever they are needed, and use an IdentityPasswordProvider (which provides passwords for encrypted private key files) that uses the Eclipse secure storage for key passphrases to avoid repeatedly asking the user for the same passphrases for the same keys. Bug: 520927 JGit-Dependency: Iaa78bbb998a5e574fa091664b75c48a3b9cfb897 Change-Id: Id3cf850c4e132e864eab7eda52c20ff379e2b1d9 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.egit.core')
-rw-r--r--org.eclipse.egit.core/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/Activator.java104
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferenceInitializer.java1
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferences.java14
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java15
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/internal/EGitSshdSessionFactory.java217
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/internal/SshPreferencesMirror.java183
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties6
-rw-r--r--org.eclipse.egit.core/src/org/eclipse/egit/core/securestorage/EGitSecureStore.java25
9 files changed, 537 insertions, 29 deletions
diff --git a/org.eclipse.egit.core/META-INF/MANIFEST.MF b/org.eclipse.egit.core/META-INF/MANIFEST.MF
index 252abaa163..11bd4f5c89 100644
--- a/org.eclipse.egit.core/META-INF/MANIFEST.MF
+++ b/org.eclipse.egit.core/META-INF/MANIFEST.MF
@@ -69,6 +69,7 @@ Import-Package: com.jcraft.jsch;bundle-version="[0.1.37,0.2.0)",
org.eclipse.jgit.storage.file;version="[5.2.0,5.3.0)",
org.eclipse.jgit.submodule;version="[5.2.0,5.3.0)",
org.eclipse.jgit.transport;version="[5.2.0,5.3.0)",
+ org.eclipse.jgit.transport.sshd;version="[5.2.0,5.3.0)",
org.eclipse.jgit.treewalk;version="[5.2.0,5.3.0)",
org.eclipse.jgit.treewalk.filter;version="[5.2.0,5.3.0)",
org.eclipse.jgit.util;version="[5.2.0,5.3.0)",
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/Activator.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/Activator.java
index 53e3d6647f..1ffe5ee656 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/Activator.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/Activator.java
@@ -52,9 +52,12 @@ import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.egit.core.internal.CoreText;
+import org.eclipse.egit.core.internal.EGitSshdSessionFactory;
import org.eclipse.egit.core.internal.ReportingTypedConfigGetter;
+import org.eclipse.egit.core.internal.SshPreferencesMirror;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
import org.eclipse.egit.core.internal.job.JobUtil;
import org.eclipse.egit.core.internal.trace.GitTraceLocation;
@@ -71,6 +74,7 @@ import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jsch.core.IJSchService;
@@ -80,12 +84,18 @@ import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.RepositoryProvider;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
/**
* The plugin class for the org.eclipse.egit.core plugin. This
* is a singleton class.
*/
public class Activator extends Plugin implements DebugOptionsListener {
+
+ private enum SshClientType {
+ JSCH, APACHE
+ }
+
private static Activator plugin;
private static String pluginId;
private RepositoryCache repositoryCache;
@@ -96,6 +106,8 @@ public class Activator extends Plugin implements DebugOptionsListener {
private IResourceChangeListener preDeleteProjectListener;
private IgnoreDerivedResources ignoreDerivedResourcesListener;
private MergeStrategyRegistryListener mergeStrategyRegistryListener;
+ private IPreferenceChangeListener sshClientChangeListener;
+ private ServiceTracker<IProxyService, IProxyService> proxyServiceTracker;
/**
* @return the singleton {@link Activator}
@@ -181,10 +193,10 @@ public class Activator extends Plugin implements DebugOptionsListener {
public void start(final BundleContext context) throws Exception {
super.start(context);
+ pluginId = context.getBundle().getSymbolicName();
SystemReader.setInstance(
new EclipseSystemReader(SystemReader.getInstance()));
- pluginId = context.getBundle().getSymbolicName();
Config.setTypedConfigGetter(new ReportingTypedConfigGetter());
// we want to be notified about debug options changes
@@ -193,8 +205,19 @@ public class Activator extends Plugin implements DebugOptionsListener {
context.registerService(DebugOptionsListener.class.getName(), this,
props);
+ SshPreferencesMirror.INSTANCE.start();
+ proxyServiceTracker = new ServiceTracker<>(context,
+ IProxyService.class.getName(), null);
+ proxyServiceTracker.open();
setupSSH(context);
- setupProxy(context);
+ sshClientChangeListener = event -> {
+ if (GitCorePreferences.core_sshClient.equals(event.getKey())) {
+ setupSSH(getBundle().getBundleContext());
+ }
+ };
+ InstanceScope.INSTANCE.getNode(pluginId)
+ .addPreferenceChangeListener(sshClientChangeListener);
+ setupProxy();
repositoryCache = new RepositoryCache();
indexDiffCache = new IndexDiffCache();
@@ -218,25 +241,49 @@ public class Activator extends Plugin implements DebugOptionsListener {
@SuppressWarnings("unchecked")
private void setupSSH(final BundleContext context) {
- final ServiceReference ssh;
-
- ssh = context.getServiceReference(IJSchService.class.getName());
- if (ssh != null) {
- SshSessionFactory.setInstance(new EclipseSshSessionFactory(
- (IJSchService) context.getService(ssh)));
+ String sshClient = Platform.getPreferencesService().getString(pluginId,
+ GitCorePreferences.core_sshClient, "jsch", null); //$NON-NLS-1$
+ SshSessionFactory previous = SshSessionFactory.getInstance();
+ if (SshClientType.APACHE.name().equalsIgnoreCase(sshClient)) {
+ if (previous instanceof EGitSshdSessionFactory) {
+ return;
+ }
+ logInfo(CoreText.Activator_SshClientUsingApache);
+ SshSessionFactory.setInstance(new EGitSshdSessionFactory());
+ } else {
+ if (previous instanceof EclipseSshSessionFactory) {
+ return;
+ }
+ if (!SshClientType.JSCH.name().equalsIgnoreCase(sshClient)) {
+ logWarning(
+ MessageFormat.format(
+ CoreText.Activator_SshClientUnknown, sshClient),
+ null);
+ }
+ ServiceReference ssh = context
+ .getServiceReference(IJSchService.class.getName());
+ if (ssh != null) {
+ SshSessionFactory.setInstance(new EclipseSshSessionFactory(
+ (IJSchService) context.getService(ssh)));
+ } else {
+ // Should never happen
+ logWarning(CoreText.Activator_SshClientNoJsch, null);
+ if (previous instanceof EGitSshdSessionFactory) {
+ return;
+ }
+ SshSessionFactory.setInstance(new EGitSshdSessionFactory());
+ }
+ }
+ if (previous instanceof SshdSessionFactory) {
+ ((SshdSessionFactory) previous).close();
}
}
- @SuppressWarnings("unchecked")
- private void setupProxy(final BundleContext context) {
- final ServiceReference proxy;
-
- proxy = context.getServiceReference(IProxyService.class.getName());
+ private void setupProxy() {
+ IProxyService proxy = getProxyService();
if (proxy != null) {
- ProxySelector.setDefault(new EclipseProxySelector(
- (IProxyService) context.getService(proxy)));
- Authenticator.setDefault(new EclipseAuthenticator(
- (IProxyService) context.getService(proxy)));
+ ProxySelector.setDefault(new EclipseProxySelector(proxy));
+ Authenticator.setDefault(new EclipseAuthenticator(proxy));
}
}
@@ -386,8 +433,31 @@ public class Activator extends Plugin implements DebugOptionsListener {
return secureStore;
}
+ /**
+ * Obtains the {@link IProxyService}.
+ *
+ * @return the {@link IProxyService} or {@code null} if none is available.
+ */
+ public IProxyService getProxyService() {
+ return proxyServiceTracker.getService();
+ }
+
@Override
public void stop(final BundleContext context) throws Exception {
+ SshPreferencesMirror.INSTANCE.stop();
+ if (sshClientChangeListener != null) {
+ InstanceScope.INSTANCE.getNode(pluginId)
+ .removePreferenceChangeListener(sshClientChangeListener);
+ sshClientChangeListener = null;
+ }
+ SshSessionFactory current = SshSessionFactory.getInstance();
+ if (current instanceof SshdSessionFactory) {
+ ((SshdSessionFactory) current).close();
+ }
+ if (proxyServiceTracker != null) {
+ proxyServiceTracker.close();
+ proxyServiceTracker = null;
+ }
if (mergeStrategyRegistryListener != null) {
Platform.getExtensionRegistry()
.removeListener(mergeStrategyRegistryListener);
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferenceInitializer.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferenceInitializer.java
index 9ef469c2e9..06116311b2 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferenceInitializer.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferenceInitializer.java
@@ -37,6 +37,7 @@ public class GitCorePreferenceInitializer extends AbstractPreferenceInitializer
String defaultRepoDir = RepositoryUtil.getDefaultDefaultRepositoryDir();
p.put(GitCorePreferences.core_defaultRepositoryDir, defaultRepoDir);
p.putInt(GitCorePreferences.core_maxPullThreadsCount, 1);
+ p.put(GitCorePreferences.core_sshClient, "jsch"); //$NON-NLS-1$
}
}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferences.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferences.java
index 027c941476..43ab5ad359 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferences.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/GitCorePreferences.java
@@ -14,7 +14,12 @@
package org.eclipse.egit.core;
/** Preferences used by the core plugin. */
-public class GitCorePreferences {
+public final class GitCorePreferences {
+
+ private GitCorePreferences() {
+ // No instantiation
+ }
+
/** */
public static final String core_packedGitWindowSize =
"core_packedGitWindowSize"; //$NON-NLS-1$
@@ -74,4 +79,11 @@ public class GitCorePreferences {
* Max number of simultaneous pull jobs, default is one.
*/
public static final String core_maxPullThreadsCount = "core_max_pull_threads_count"; //$NON-NLS-1$
+
+ /**
+ * Ssh client library to use. Currently allowed values are "jsch" and
+ * "apache", case insensitive, if undefined or any other value the default
+ * "jsch" will be used.
+ */
+ public static final String core_sshClient = "core_ssh_client"; //$NON-NLS-1$
}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
index f9bcabdd6e..c1de1cf76a 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java
@@ -52,6 +52,15 @@ public class CoreText extends NLS {
public static String Activator_ReconfigureWindowCacheError;
/** */
+ public static String Activator_SshClientNoJsch;
+
+ /** */
+ public static String Activator_SshClientUnknown;
+
+ /** */
+ public static String Activator_SshClientUsingApache;
+
+ /** */
public static String AssumeUnchangedOperation_adding;
/** */
@@ -504,6 +513,12 @@ public class CoreText extends NLS {
/** */
public static String ReportingTypedConfigGetter_invalidConfigWithLocationIgnored;
+ /** */
+ public static String SshPreferencesMirror_invalidDirectory;
+
+ /** */
+ public static String SshPreferencesMirror_invalidKeyFile;
+
static {
initializeMessages(BUNDLE_NAME, CoreText.class);
}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/EGitSshdSessionFactory.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/EGitSshdSessionFactory.java
new file mode 100644
index 0000000000..e36a75d094
--- /dev/null
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/EGitSshdSessionFactory.java
@@ -0,0 +1,217 @@
+/*******************************************************************************
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.egit.core.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.core.net.proxy.IProxyData;
+import org.eclipse.core.net.proxy.IProxyService;
+import org.eclipse.egit.core.Activator;
+import org.eclipse.egit.core.securestorage.EGitSecureStore;
+import org.eclipse.egit.core.securestorage.UserPasswordCredentials;
+import org.eclipse.equinox.security.storage.StorageException;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
+import org.eclipse.jgit.transport.sshd.ProxyData;
+import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
+import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
+
+/**
+ * A bridge between the Eclipse ssh2 configuration (which originally was done
+ * for JSch) and JGit's Apache MINA {@link SshdSessionFactory}.
+ */
+public class EGitSshdSessionFactory extends SshdSessionFactory {
+
+ /**
+ * Creates a new {@link EGitSshdSessionFactory}. It doesn't use a key
+ * cache, and a proxy database based on the {@link IProxyService}.
+ */
+ public EGitSshdSessionFactory() {
+ super(null, new EGitProxyDataFactory());
+ }
+
+ @Override
+ public File getSshDirectory() {
+ File file = super.getSshDirectory();
+ if (file != null) {
+ // Someone explicitly set an ssh directory: use it
+ return file;
+ }
+ return SshPreferencesMirror.INSTANCE.getSshDirectory();
+ }
+
+ @Override
+ @NonNull
+ protected List<Path> getDefaultIdentities(@NonNull File sshDir) {
+ List<Path> defaultKeys = SshPreferencesMirror.INSTANCE
+ .getDefaultIdentities(sshDir);
+ if (defaultKeys == null) {
+ // None configured
+ return Collections.emptyList();
+ } else if (defaultKeys.isEmpty()) {
+ // Something configured, but invalid: use default
+ return super.getDefaultIdentities(sshDir);
+ }
+ return defaultKeys;
+ }
+
+ @Override
+ protected String getDefaultPreferredAuthentications() {
+ return SshPreferencesMirror.INSTANCE.getPreferredAuthentications();
+ }
+
+ @Override
+ protected KeyPasswordProvider createKeyPasswordProvider(
+ CredentialsProvider provider) {
+ return new EGitFilePasswordProvider(provider,
+ Activator.getDefault().getSecureStore());
+ }
+
+ private static class EGitProxyDataFactory implements ProxyDataFactory {
+
+ @Override
+ public ProxyData get(InetSocketAddress remoteAddress) {
+ IProxyService service = Activator.getDefault().getProxyService();
+ if (service == null || !service.isProxiesEnabled()) {
+ return null;
+ }
+ try {
+ IProxyData[] data = service
+ .select(new URI(IProxyData.SOCKS_PROXY_TYPE,
+ "//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$
+ if (data == null || data.length == 0) {
+ data = service.select(new URI(IProxyData.HTTP_PROXY_TYPE,
+ "//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$
+ if (data == null || data.length == 0) {
+ return null;
+ }
+ }
+ return newData(data[0]);
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ private ProxyData newData(IProxyData data) {
+ if (data == null) {
+ return null;
+ }
+ InetSocketAddress proxyAddress = new InetSocketAddress(
+ data.getHost(), data.getPort());
+ char[] password = null;
+ try {
+ password = data.getPassword() == null ? null
+ : data.getPassword().toCharArray();
+ Proxy proxy;
+ switch (data.getType()) {
+ case IProxyData.HTTP_PROXY_TYPE:
+ proxy = new Proxy(Proxy.Type.HTTP, proxyAddress);
+ return new ProxyData(proxy, data.getUserId(), password);
+ case IProxyData.SOCKS_PROXY_TYPE:
+ proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
+ return new ProxyData(proxy, data.getUserId(), password);
+ default:
+ return null;
+ }
+ } finally {
+ if (password != null) {
+ Arrays.fill(password, '\000');
+ }
+ }
+ }
+ }
+
+ private static class EGitFilePasswordProvider
+ extends IdentityPasswordProvider {
+
+ private final EGitSecureStore store;
+
+ public EGitFilePasswordProvider(CredentialsProvider provider,
+ EGitSecureStore store) {
+ super(provider);
+ this.store = store;
+ }
+
+ @Override
+ protected char[] getPassword(URIish uri, int attempt,
+ @NonNull State state) throws IOException {
+ if (attempt == 0) {
+ // Obtain a password from secure store and return it if
+ // successful
+ try {
+ UserPasswordCredentials credentials = store
+ .getCredentials(uri);
+ if (credentials != null) {
+ String password = credentials.getPassword();
+ if (password != null) {
+ char[] pass = password.toCharArray();
+ state.setPassword(pass);
+ // Don't increment the count; this attempt shall not
+ // count against the limit, and we rely on count
+ // still being zero when we handle the result.
+ return pass;
+ }
+ }
+ } catch (StorageException | RuntimeException e) {
+ Activator.logError(e.getMessage(), e);
+ }
+ }
+ return super.getPassword(uri, attempt, state);
+ }
+
+ @Override
+ protected boolean keyLoaded(URIish uri, State state, char[] password,
+ Exception err)
+ throws IOException, GeneralSecurityException {
+ if (state != null && password != null) {
+ if (state.getCount() == 0) {
+ // We tried the secure store.
+ if (err != null) {
+ // Clear the secure store entry for this resource -- it
+ // didn't work. On the next round we'll not find a
+ // password in the secure store, increment the count,
+ // and go through the CredentialsProvider.
+ try {
+ store.clearCredentials(uri);
+ } catch (IOException | RuntimeException e) {
+ Activator.logError(e.getMessage(), e);
+ }
+ return true; // Re-try
+ }
+ } else if (err == null) {
+ // A user-entered password worked: store it in the secure
+ // store. We need a dummy user name to go with it.
+ UserPasswordCredentials credentials = new UserPasswordCredentials(
+ "egit:ssh:resource", new String(password)); //$NON-NLS-1$
+ try {
+ store.putCredentials(uri, credentials);
+ } catch (StorageException | RuntimeException e) {
+ Activator.logError(e.getMessage(), e);
+ }
+ }
+ }
+ return super.keyLoaded(uri, state, password, err);
+ }
+ }
+}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/SshPreferencesMirror.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/SshPreferencesMirror.java
new file mode 100644
index 0000000000..487f49c69e
--- /dev/null
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/SshPreferencesMirror.java
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.egit.core.internal;
+
+import static java.text.MessageFormat.format;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.egit.core.Activator;
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * Mirrors the Eclipse ssh-related preferences. The values are mirrored in
+ * formats suitable for the Apache MINA sshd client implementation, and are
+ * updated when the preferences change.
+ * <p>
+ * All operations are thread-safe.
+ * </p>
+ */
+public class SshPreferencesMirror {
+
+ // No listener support: it's probably not a good idea or even impossible to
+ // reconfigure ongoing ssh sessions. Our session factory simply gets the
+ // current values whenever a new session is started.
+
+ /** The singleton instance of the {@link SshPreferencesMirror}. */
+ public static final SshPreferencesMirror INSTANCE = new SshPreferencesMirror();
+
+ private IEclipsePreferences preferences;
+
+ private IPreferenceChangeListener listener = event -> reloadPreferences();
+
+ private File sshDirectory;
+
+ private List<String> defaultIdentities;
+
+ private String defaultMechanisms;
+
+ private SshPreferencesMirror() {
+ // This is a singleton.
+ }
+
+ /** Starts mirroring the ssh preferences. */
+ public void start() {
+ preferences = InstanceScope.INSTANCE.getNode("org.eclipse.jsch.core"); //$NON-NLS-1$
+ if (preferences == null) {
+ return;
+ }
+ preferences.addPreferenceChangeListener(listener);
+ reloadPreferences();
+ }
+
+ /** Stops mirroring the ssh preferences. */
+ public void stop() {
+ if (preferences != null) {
+ preferences.removePreferenceChangeListener(listener);
+ }
+ }
+
+ private void reloadPreferences() {
+ synchronized (this) {
+ setSshDirectory();
+ setDefaultIdentities();
+ setPreferredAuthentications();
+ }
+ }
+
+ private String get(@NonNull String key) {
+ IEclipsePreferences pref = preferences;
+ return pref == null ? null : pref.get(key, null);
+ }
+
+ private void setSshDirectory() {
+ String sshDir = get("SSH2HOME"); //$NON-NLS-1$
+ if (sshDir != null) {
+ try {
+ sshDirectory = Paths.get(sshDir).toFile();
+ } catch (InvalidPathException e) {
+ Activator.logWarning(
+ format(CoreText.SshPreferencesMirror_invalidDirectory,
+ sshDir),
+ null);
+ }
+ }
+ sshDirectory = null;
+ }
+
+ private void setDefaultIdentities() {
+ String defaultKeys = get("PRIVATEKEY"); //$NON-NLS-1$
+ if (defaultKeys == null || defaultKeys.isEmpty()) {
+ defaultIdentities = null;
+ return;
+ }
+ defaultIdentities = Arrays.asList(defaultKeys.trim().split("\\s*,\\s*")) //$NON-NLS-1$
+ .stream().map(s -> {
+ if (s.isEmpty()) {
+ return null;
+ }
+ try {
+ Paths.get(s);
+ return s;
+ } catch (InvalidPathException e) {
+ Activator.logWarning(
+ format(CoreText.SshPreferencesMirror_invalidKeyFile,
+ s),
+ null);
+ return null;
+ }
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ private void setPreferredAuthentications() {
+ String mechanisms = get("CVSSSH2PreferencePage.PREF_AUTH_METHODS"); //$NON-NLS-1$
+ if (mechanisms == null || mechanisms.isEmpty()) {
+ defaultMechanisms = null;
+ }
+ defaultMechanisms = mechanisms;
+ }
+
+ /**
+ * Gets the ssh directory.
+ *
+ * @return the configured ssh directory, or {@code null} if the
+ * configuration is invalid
+ */
+ public File getSshDirectory() {
+ synchronized (this) {
+ return sshDirectory;
+ }
+ }
+
+ /**
+ * Gets the configured default key files.
+ *
+ * @param sshDir
+ * the directory that represents ~/.ssh/
+ * @return a possibly empty list of paths containing the configured default
+ * identities (private keys), or {@code null} if the user didn't
+ * configure any. An empty list indicates that user did configure
+ * something invalid.
+ */
+ public List<Path> getDefaultIdentities(@NonNull File sshDir) {
+ synchronized (this) {
+ if (defaultIdentities == null) {
+ return null;
+ }
+ return defaultIdentities.stream()
+ .map(s -> new File(sshDir, s).toPath())
+ .filter(Files::exists).collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * Gets the configured default authentication mechanisms.
+ *
+ * @return the default authentication mechanisms as a single comma-separated
+ * string
+ */
+ public String getPreferredAuthentications() {
+ synchronized (this) {
+ return defaultMechanisms;
+ }
+ }
+}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
index d8293b640d..d0d42240ad 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties
@@ -48,6 +48,9 @@ Activator_AutoSharingFailed=Auto sharing project with git failed
Activator_ignoreResourceFailed=Ignoring {0} failed
Activator_noBuiltinLfsSupportDetected=Builtin LFS support not present/detected
Activator_ReconfigureWindowCacheError=Exception when reconfiguring window cache from configuration, default configuration will be used
+Activator_SshClientNoJsch=JSch configured as ssh library, but JSch service cannot be found. Using Apache sshd library instead.
+Activator_SshClientUnknown=Unknown ssh library ''{0}'' configured; valid values are "jsch" or "apache". Using JSch.
+Activator_SshClientUsingApache=Using Apache MINA sshd as ssh client.
AssumeUnchangedOperation_adding=Marking resources unchanged
AssumeUnchangedOperation_writingIndex=Writing index for {0}
@@ -206,3 +209,6 @@ ReportingTypedConfigGetter_invalidConfig=Git config value ''{0}'' is invalid; us
ReportingTypedConfigGetter_invalidConfigIgnored=Ignored invalid git config value ''{0}''
ReportingTypedConfigGetter_invalidConfigWithLocation=Git config ''{0}'': value ''{1}'' is invalid; using default value ''{2}''
ReportingTypedConfigGetter_invalidConfigWithLocationIgnored=Git config ''{0}'': ignored invalid value ''{1}''
+
+SshPreferencesMirror_invalidDirectory=Invalid ssh home directory configured: {0}
+SshPreferencesMirror_invalidKeyFile=Invalid ssh key file configured: {0}
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/securestorage/EGitSecureStore.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/securestorage/EGitSecureStore.java
index 07148e223b..38995c2173 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/securestorage/EGitSecureStore.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/securestorage/EGitSecureStore.java
@@ -88,17 +88,20 @@ public class EGitSecureStore {
}
static String calcNodePath(URIish uri) {
- URIish storedURI = uri.setUser(null).setPass(null).setPath(null);
- if (uri.getPort() == -1) {
- String s = uri.getScheme();
- if ("http".equals(s)) //$NON-NLS-1$
- storedURI = storedURI.setPort(80);
- else if ("https".equals(s)) //$NON-NLS-1$
- storedURI = storedURI.setPort(443);
- else if ("ssh".equals(s) || "sftp".equals(s)) //$NON-NLS-1$ //$NON-NLS-2$
- storedURI = storedURI.setPort(22);
- else if ("ftp".equals(s)) //$NON-NLS-1$
- storedURI = storedURI.setPort(21);
+ URIish storedURI = uri.setUser(null).setPass(null);
+ if (uri.getScheme() != null && !"file".equals(uri.getScheme())) { //$NON-NLS-1$
+ storedURI = storedURI.setPath(null);
+ if (uri.getPort() == -1) {
+ String s = uri.getScheme();
+ if ("http".equals(s)) //$NON-NLS-1$
+ storedURI = storedURI.setPort(80);
+ else if ("https".equals(s)) //$NON-NLS-1$
+ storedURI = storedURI.setPort(443);
+ else if ("ssh".equals(s) || "sftp".equals(s)) //$NON-NLS-1$ //$NON-NLS-2$
+ storedURI = storedURI.setPort(22);
+ else if ("ftp".equals(s)) //$NON-NLS-1$
+ storedURI = storedURI.setPort(21);
+ }
}
String pathName = GIT_PATH_PREFIX
+ EncodingUtils.encodeSlashes(storedURI.toString());

Back to the top