Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Sohn2020-11-28 20:02:09 +0000
committerMatthias Sohn2020-11-28 20:51:50 +0000
commit286ad23cb56ffeac77d4bfd03be575358fd5217c (patch)
treedc075c6b1d1e813f253ec465813b6251d9d07d61 /org.eclipse.jgit.ssh.apache/src
parent4887894ffd637030a311ca8d60b78515b1a5cf35 (diff)
parent4f2065d145c3fa3f8ad3de28af3ee5dfeb74c765 (diff)
downloadjgit-286ad23cb56ffeac77d4bfd03be575358fd5217c.tar.gz
jgit-286ad23cb56ffeac77d4bfd03be575358fd5217c.tar.xz
jgit-286ad23cb56ffeac77d4bfd03be575358fd5217c.zip
Merge branch 'master' into next
* master: Remove unused imports Silence API warnings Remove erraneously merged source features Add support for reading symrefs from pack capabilities Prepare 5.3.9-SNAPSHOT builds JGit v5.3.8.202011260953-r Prepare 5.1.15-SNAPSHOT builds JGit v5.1.14.202011251942-r GC#deleteOrphans: log warning for deleted orphaned files GC#deleteOrphans: handle failure to list files in pack directory Ensure that GC#deleteOrphans respects pack lock Prepare 5.10.0-SNAPSHOT builds JGit v5.10.0.202011251205-m3 PacketLineIn: ensure that END != DELIM Update Orbit to S20201118210000 and add target for 4.18 PacketLineIn: ensure that END != DELIM PacketLineIn: ensure that END != DELIM Allow to resolve a conflict by checking out a file Update Orbit to I20201111205634 Document that setLastModified sets time of symlink target Fix bug in PerformanceLogContext Fix IOException occurring during gc Prepare 5.10.0-SNAPSHOT builds JGit v5.10.0.202011041322-m2 Revert "Client-side protocol V2 support for fetching" Close Repository to fix tests failing on Windows Client-side protocol V2 support for fetching Update slf4j to 1.7.30 Update Orbit to S20201027182932 (2020-12 M2) Fix formatting of config option values Document options in core section supported by JGit Ensure .gitmodules is loaded when accessing submodule name Export new package org.eclipse.jgit.logging and import it where used Ensure GC.deleteOrphans() can delete read-only orphaned files on Windows Add new performance logging Implement git describe --all Compute time differences with Duration Override config http.userAgent from environment GIT_HTTP_USER_AGENT Upgrade spotbugs-maven-plugin to 4.1.3 Fix OperatorPrecedence warning flagged by error prone UploadPackTest#testUploadRedundantBytes: ensure test repo is closed ObjectDirectory#selectObjectRepresentation: fix formatting Upgrade ecj to 3.23.0 Support "http.userAgent" and "http.extraHeader" from the git config sshd: better error report when user cancels authentication API filters for PackStatistics.Accumulator Add TypedConfigGetter.getPath() Make Javadoc consistent for PackStatistics fields Measure time taken for reachability checks Measure time taken for negotiation in protocol V2 IndexDiffFilter: handle path prefixes correctly sshd: support the ProxyJump ssh config Upgrade jacoco-maven-plugin to 0.8.6 ReceivePackStats: Add size and count of unnecessary pushed objects Upgrade maven-project-info-reports-plugin to 3.1.1 Prepare 5.9.1-SNAPSHOT builds JGit v5.9.0.202009080501-r [releng] Enable japicmp for the fragments added in 5.8.0 GitlinkMergeTest: fix boxing warnings Remove unused API problem filters Add missing since tag on BundleWriter#addObjectsAsIs SshdSession: close channel gracefully GPG: include signer's user ID in the signature jgit: Add DfsBundleWriter Bump Bazel version to 3.5.0 Upgrade maven-resources-plugin to 3.2.0 Upgrade plexus-compiler version to 2.8.8 [bazel] Add missing dependency to slf4j-api [errorprone] DirCacheEntry: make clear operator precedence [errorprone] PackWriter#parallelDeltaSearch: avoid suppressed exception [errorprone] Declare DirCache#version final Add jgit-4.17-staging target platform for 2020-09 Update target platform to R20200831200620 Prepare 5.10.0-SNAPSHOT builds Prepare 5.9.0-SNAPSHOT builds ResolveMerger: do not content-merge gitlinks on del/mod conflicts ResolveMerger: Adding test cases for GITLINK deletion ResolveMerger: choose OURS on gitlink when ignoreConflicts ResolveMerger: improving content merge readability ResolveMerger: extracting createGitLinksMergeResult method ResolveMerger: Adding test cases for GITLINK merge JGit v5.9.0.202008260805-m3 Fix possible NegativeArraySizeException in PackIndexV1 FS: use binary search to determine filesystem timestamp resolution Do not prematurely create directory of jgit's XDG config file FS: write to JGit config in a background thread FS: don't cache fallback if running in background Keep line endings for text files committed with CR/LF on text=auto Delay WindowCache statistics JMX MBean registration [releng] Update plexus-compiler to 2.8.7 DirCache: support index V4 Update javadoc for RemoteSession and SshSessionFactory Fix JSchProcess.waitFor() with time-out sshd: work around a race condition in Apache MINA sshd 2.4.0/2.5.x sshd: store per-session data on the sshd session object FilterSpec: Use BigInteger.ZERO instead of valueOf(0) Do not send empty blob in response to blob:none filter Add support for tree filters when fetching sshd: use PropertyResolver in test FS_POSIX: avoid prompt to install the XCode tools on OS X Remove dependency on JSch from SSH test framework Use LinkedBlockingQueue for executor determining filesystem attributes Update API warning filters Remove unused imports Bazel: Add workspace status command to stamp final artifact DiffFormatter: correctly deal with tracked files in ignored folders Prepare 5.8.2-SNAPSHOT builds JGit v5.8.1.202007141445-r Update Jetty to 9.4.30.v20200611 Fix writing GPG signatures with trailing newline Rename a test method Add a test for upstream bug SSHD-1028 Improve error message when receive.maxCommandBytes is exceeded LfsConnectionFactory#getLfsUrl: Fix unconditional break in for-loop DiffFormatterTest: Add a test to confirm the default rename detection settings Upgrade maven-site-plugin to 3.9.1 Upgrade build-helper-maven-plugin to 3.2.0 Upgrade spotbugs to 4.0.4 MergedReftable: Include the last reftable in determining minUpdateIndex Add new osgi fragments to maven-central deploy scripts PackBitmapIndex: Not buffer inflated bitmap during bitmap creation. Do not require org.assertj.core.annotations Upgrade ecj to 3.22.0 Remove workaround for signing jars using Tycho plugins Use https for URL of jgit website Fix CI information in pom.xml Use gitiles as scm url in pom.xml for browsing source code Update API baseline to 5.8.0.202006091008-r Remove trailing whitespace Change-Id: Ie6bc6954741a47cfbd32c0886bdbd7b594f08b31 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src')
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationCanceledException.java30
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java257
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java4
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java113
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java69
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java41
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java8
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java51
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java72
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java245
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java31
11 files changed, 706 insertions, 215 deletions
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationCanceledException.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationCanceledException.java
new file mode 100644
index 0000000000..aa4623571d
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationCanceledException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> 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.sshd;
+
+import java.util.concurrent.CancellationException;
+
+/**
+ * An exception to report that the user canceled the SSH authentication.
+ */
+public class AuthenticationCanceledException extends CancellationException {
+
+ // If this is not a CancellationException sshd will try other authentication
+ // mechanisms.
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new {@link AuthenticationCanceledException}.
+ */
+ public AuthenticationCanceledException() {
+ super(SshdText.get().authenticationCanceled);
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
index 420a1d16eb..0d6f3027f2 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
@@ -18,21 +18,30 @@ import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSessionImpl;
+import org.apache.sshd.client.session.ClientUserAuthService;
+import org.apache.sshd.common.AttributeRepository;
import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.PropertyResolver;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.kex.KexState;
import org.apache.sshd.common.util.Readable;
import org.apache.sshd.common.util.buffer.Buffer;
import org.eclipse.jgit.errors.InvalidPatternException;
@@ -68,6 +77,17 @@ public class JGitClientSession extends ClientSessionImpl {
private volatile StatefulProxyConnector proxyHandler;
/**
+ * Work-around for bug 565394 / SSHD-1050; remove when using sshd 2.6.0.
+ */
+ private volatile AuthFuture authFuture;
+
+ /** Records exceptions before there is an authFuture. */
+ private List<Throwable> earlyErrors = new ArrayList<>();
+
+ /** Guards setting an earlyError and the authFuture together. */
+ private final Object errorLock = new Object();
+
+ /**
* @param manager
* @param session
* @throws Exception
@@ -77,6 +97,125 @@ public class JGitClientSession extends ClientSessionImpl {
super(manager, session);
}
+ // BEGIN Work-around for bug 565394 / SSHD-1050
+ // Remove when using sshd 2.6.0.
+
+ @Override
+ public AuthFuture auth() throws IOException {
+ if (getUsername() == null) {
+ throw new IllegalStateException(
+ SshdText.get().sessionWithoutUsername);
+ }
+ ClientUserAuthService authService = getUserAuthService();
+ String serviceName = nextServiceName();
+ List<Throwable> errors = null;
+ AuthFuture future;
+ // Guard both getting early errors and setting authFuture
+ synchronized (errorLock) {
+ future = authService.auth(serviceName);
+ if (future == null) {
+ // Internal error; no translation.
+ throw new IllegalStateException(
+ "No auth future generated by service '" //$NON-NLS-1$
+ + serviceName + '\'');
+ }
+ errors = earlyErrors;
+ earlyErrors = null;
+ authFuture = future;
+ }
+ if (errors != null && !errors.isEmpty()) {
+ Iterator<Throwable> iter = errors.iterator();
+ Throwable first = iter.next();
+ iter.forEachRemaining(t -> {
+ if (t != first && t != null) {
+ first.addSuppressed(t);
+ }
+ });
+ // Mark the future as having had an exception; just to be on the
+ // safe side. Actually, there shouldn't be anyone waiting on this
+ // future yet.
+ future.setException(first);
+ if (log.isDebugEnabled()) {
+ log.debug("auth({}) early exception type={}: {}", //$NON-NLS-1$
+ this, first.getClass().getSimpleName(),
+ first.getMessage());
+ }
+ if (first instanceof SshException) {
+ throw new SshException(
+ ((SshException) first).getDisconnectCode(),
+ first.getMessage(), first);
+ }
+ throw new IOException(first.getMessage(), first);
+ }
+ return future;
+ }
+
+ @Override
+ protected void signalAuthFailure(AuthFuture future, Throwable t) {
+ signalAuthFailure(t);
+ }
+
+ private void signalAuthFailure(Throwable t) {
+ AuthFuture future = authFuture;
+ if (future == null) {
+ synchronized (errorLock) {
+ if (earlyErrors != null) {
+ earlyErrors.add(t);
+ }
+ future = authFuture;
+ }
+ }
+ if (future != null) {
+ future.setException(t);
+ }
+ if (log.isDebugEnabled()) {
+ boolean signalled = future != null && t == future.getException();
+ log.debug("signalAuthFailure({}) type={}, signalled={}: {}", this, //$NON-NLS-1$
+ t.getClass().getSimpleName(), Boolean.valueOf(signalled),
+ t.getMessage());
+ }
+ }
+
+ @Override
+ public void exceptionCaught(Throwable t) {
+ signalAuthFailure(t);
+ super.exceptionCaught(t);
+ }
+
+ @Override
+ protected void preClose() {
+ signalAuthFailure(
+ new SshException(SshdText.get().authenticationOnClosedSession));
+ super.preClose();
+ }
+
+ @Override
+ protected void handleDisconnect(int code, String msg, String lang,
+ Buffer buffer) throws Exception {
+ signalAuthFailure(new SshException(code, msg));
+ super.handleDisconnect(code, msg, lang, buffer);
+ }
+
+ @Override
+ protected <C extends Collection<ClientSessionEvent>> C updateCurrentSessionState(
+ C newState) {
+ if (closeFuture.isClosed()) {
+ newState.add(ClientSessionEvent.CLOSED);
+ }
+ if (isAuthenticated()) { // authFuture.isSuccess()
+ newState.add(ClientSessionEvent.AUTHED);
+ }
+ if (KexState.DONE.equals(getKexState())) {
+ AuthFuture future = authFuture;
+ if (future == null || future.isFailure()) {
+ newState.add(ClientSessionEvent.WAIT_AUTH);
+ }
+ }
+ return newState;
+ }
+
+ // END Work-around for bug 565394 / SSHD-1050
+
/**
* Retrieves the {@link HostConfigEntry} this session was created for.
*
@@ -419,4 +558,122 @@ public class JGitClientSession extends ClientSessionImpl {
return b.toString();
}
+ @Override
+ public <T> T getAttribute(AttributeKey<T> key) {
+ T value = super.getAttribute(key);
+ if (value == null) {
+ IoSession ioSession = getIoSession();
+ if (ioSession != null) {
+ Object obj = ioSession.getAttribute(AttributeRepository.class);
+ if (obj instanceof AttributeRepository) {
+ AttributeRepository sessionAttributes = (AttributeRepository) obj;
+ value = sessionAttributes.resolveAttribute(key);
+ }
+ }
+ }
+ return value;
+ }
+
+ @Override
+ public PropertyResolver getParentPropertyResolver() {
+ IoSession ioSession = getIoSession();
+ if (ioSession != null) {
+ Object obj = ioSession.getAttribute(AttributeRepository.class);
+ if (obj instanceof PropertyResolver) {
+ return (PropertyResolver) obj;
+ }
+ }
+ return super.getParentPropertyResolver();
+ }
+
+ /**
+ * An {@link AttributeRepository} that chains together two other attribute
+ * sources in a hierarchy.
+ */
+ public static class ChainingAttributes implements AttributeRepository {
+
+ private final AttributeRepository delegate;
+
+ private final AttributeRepository parent;
+
+ /**
+ * Create a new {@link ChainingAttributes} attribute source.
+ *
+ * @param self
+ * to search for attributes first
+ * @param parent
+ * to search for attributes if not found in {@code self}
+ */
+ public ChainingAttributes(AttributeRepository self,
+ AttributeRepository parent) {
+ this.delegate = self;
+ this.parent = parent;
+ }
+
+ @Override
+ public int getAttributesCount() {
+ return delegate.getAttributesCount();
+ }
+
+ @Override
+ public <T> T getAttribute(AttributeKey<T> key) {
+ return delegate.getAttribute(Objects.requireNonNull(key));
+ }
+
+ @Override
+ public Collection<AttributeKey<?>> attributeKeys() {
+ return delegate.attributeKeys();
+ }
+
+ @Override
+ public <T> T resolveAttribute(AttributeKey<T> key) {
+ T value = getAttribute(Objects.requireNonNull(key));
+ if (value == null) {
+ return parent.getAttribute(key);
+ }
+ return value;
+ }
+ }
+
+ /**
+ * A {@link ChainingAttributes} repository that doubles as a
+ * {@link PropertyResolver}. The property map can be set via the attribute
+ * key {@link SessionAttributes#PROPERTIES}.
+ */
+ public static class SessionAttributes extends ChainingAttributes
+ implements PropertyResolver {
+
+ /** Key for storing a map of properties in the attributes. */
+ public static final AttributeKey<Map<String, Object>> PROPERTIES = new AttributeKey<>();
+
+ private final PropertyResolver parentProperties;
+
+ /**
+ * Creates a new {@link SessionAttributes} attribute and property
+ * source.
+ *
+ * @param self
+ * to search for attributes first
+ * @param parent
+ * to search for attributes if not found in {@code self}
+ * @param parentProperties
+ * to search for properties if not found in {@code self}
+ */
+ public SessionAttributes(AttributeRepository self,
+ AttributeRepository parent, PropertyResolver parentProperties) {
+ super(self, parent);
+ this.parentProperties = parentProperties;
+ }
+
+ @Override
+ public PropertyResolver getParentPropertyResolver() {
+ return parentProperties;
+ }
+
+ @Override
+ public Map<String, Object> getProperties() {
+ Map<String, Object> props = getAttribute(PROPERTIES);
+ return props == null ? Collections.emptyMap() : props;
+ }
+ }
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
index 0a7082cefe..4abd6e901a 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
@@ -9,8 +9,6 @@
*/
package org.eclipse.jgit.internal.transport.sshd;
-import java.util.concurrent.CancellationException;
-
import org.apache.sshd.client.ClientAuthenticationManager;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.auth.password.UserAuthPassword;
@@ -49,7 +47,7 @@ public class JGitPasswordAuthentication extends UserAuthPassword {
}
String password = getPassword(session, interaction);
if (password == null) {
- throw new CancellationException();
+ throw new AuthenticationCanceledException();
}
// sendPassword takes a buffer as first argument, but actually doesn't
// use it and creates its own buffer...
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
index b8dd60fb10..beaaecaac9 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -23,12 +23,16 @@ import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors;
+import org.apache.sshd.client.ClientAuthenticationManager;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.future.ConnectFuture;
@@ -45,6 +49,9 @@ import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.ChainingAttributes;
+import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.SessionAttributes;
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector;
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
import org.eclipse.jgit.transport.CredentialsProvider;
@@ -52,6 +59,7 @@ import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.sshd.KeyCache;
import org.eclipse.jgit.transport.sshd.ProxyData;
import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
+import org.eclipse.jgit.util.StringUtils;
/**
* Customized {@link SshClient} for JGit. It creates specialized
@@ -75,6 +83,16 @@ public class JGitSshClient extends SshClient {
*/
public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>();
+ /**
+ * An attribute key for storing an alternate local address to connect to if
+ * a local forward from a ProxyJump ssh config is present. If set,
+ * {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)}
+ * will not connect to the address obtained from the {@link HostConfigEntry}
+ * but to the address stored in this key (which is assumed to forward the
+ * {@code HostConfigEntry} address).
+ */
+ public static final AttributeKey<SshdSocketAddress> LOCAL_FORWARD_ADDRESS = new AttributeKey<>();
+
private KeyCache keyCache;
private CredentialsProvider credentialsProvider;
@@ -95,40 +113,72 @@ public class JGitSshClient extends SshClient {
throw new IllegalStateException("SshClient not started."); //$NON-NLS-1$
}
Objects.requireNonNull(hostConfig, "No host configuration"); //$NON-NLS-1$
- String host = ValidateUtils.checkNotNullAndNotEmpty(
+ String originalHost = ValidateUtils.checkNotNullAndNotEmpty(
hostConfig.getHostName(), "No target host"); //$NON-NLS-1$
- int port = hostConfig.getPort();
- ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port); //$NON-NLS-1$
+ int originalPort = hostConfig.getPort();
+ ValidateUtils.checkTrue(originalPort > 0, "Invalid port: %d", //$NON-NLS-1$
+ originalPort);
+ InetSocketAddress originalAddress = new InetSocketAddress(originalHost,
+ originalPort);
+ InetSocketAddress targetAddress = originalAddress;
String userName = hostConfig.getUsername();
- InetSocketAddress address = new InetSocketAddress(host, port);
- ConnectFuture connectFuture = new DefaultConnectFuture(
- userName + '@' + address, null);
+ String id = userName + '@' + originalAddress;
+ AttributeRepository attributes = chain(context, this);
+ SshdSocketAddress localForward = attributes
+ .resolveAttribute(LOCAL_FORWARD_ADDRESS);
+ if (localForward != null) {
+ targetAddress = new InetSocketAddress(localForward.getHostName(),
+ localForward.getPort());
+ id += '/' + targetAddress.toString();
+ }
+ ConnectFuture connectFuture = new DefaultConnectFuture(id, null);
SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener(
- connectFuture, userName, address, hostConfig);
- // sshd needs some entries from the host config already in the
- // constructor of the session. Put those as properties on this client,
- // where it will find them. We can set the host config only once the
- // session object has been created.
- copyProperty(
- hostConfig.getProperty(SshConstants.PREFERRED_AUTHENTICATIONS,
- getAttribute(PREFERRED_AUTHENTICATIONS)),
- PREFERRED_AUTHS);
- setAttribute(HOST_CONFIG_ENTRY, hostConfig);
- setAttribute(ORIGINAL_REMOTE_ADDRESS, address);
+ connectFuture, userName, originalAddress, hostConfig);
+ attributes = sessionAttributes(attributes, hostConfig, originalAddress);
// Proxy support
- ProxyData proxy = getProxyData(address);
- if (proxy != null) {
- address = configureProxy(proxy, address);
- proxy.clearPassword();
+ if (localForward == null) {
+ ProxyData proxy = getProxyData(targetAddress);
+ if (proxy != null) {
+ targetAddress = configureProxy(proxy, targetAddress);
+ proxy.clearPassword();
+ }
}
- connector.connect(address, this, localAddress).addListener(listener);
+ connector.connect(targetAddress, attributes, localAddress)
+ .addListener(listener);
return connectFuture;
}
- private void copyProperty(String value, String key) {
- if (value != null && !value.isEmpty()) {
- getProperties().put(key, value);
+ private AttributeRepository chain(AttributeRepository self,
+ AttributeRepository parent) {
+ if (self == null) {
+ return Objects.requireNonNull(parent);
}
+ if (parent == null || parent == self) {
+ return self;
+ }
+ return new ChainingAttributes(self, parent);
+ }
+
+ private AttributeRepository sessionAttributes(AttributeRepository parent,
+ HostConfigEntry hostConfig, InetSocketAddress originalAddress) {
+ // sshd needs some entries from the host config already in the
+ // constructor of the session. Put those into a dedicated
+ // AttributeRepository for the new session where it will find them.
+ // We can set the host config only once the session object has been
+ // created.
+ Map<AttributeKey<?>, Object> data = new HashMap<>();
+ data.put(HOST_CONFIG_ENTRY, hostConfig);
+ data.put(ORIGINAL_REMOTE_ADDRESS, originalAddress);
+ String preferredAuths = hostConfig.getProperty(
+ SshConstants.PREFERRED_AUTHENTICATIONS,
+ resolveAttribute(PREFERRED_AUTHENTICATIONS));
+ if (!StringUtils.isEmptyOrNull(preferredAuths)) {
+ data.put(SessionAttributes.PROPERTIES,
+ Collections.singletonMap(PREFERRED_AUTHS, preferredAuths));
+ }
+ return new SessionAttributes(
+ AttributeRepository.ofAttributesMap(data),
+ parent, this);
}
private ProxyData getProxyData(InetSocketAddress remoteAddress) {
@@ -219,11 +269,6 @@ public class JGitSshClient extends SshClient {
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
session.getProperties().put(PASSWORD_PROMPTS,
Integer.valueOf(numberOfPasswordPrompts));
- FilePasswordProvider passwordProvider = getFilePasswordProvider();
- if (passwordProvider instanceof RepeatingFilePasswordProvider) {
- ((RepeatingFilePasswordProvider) passwordProvider)
- .setAttempts(numberOfPasswordPrompts);
- }
List<Path> identities = hostConfig.getIdentities().stream()
.map(s -> {
try {
@@ -237,6 +282,7 @@ public class JGitSshClient extends SshClient {
.collect(Collectors.toList());
CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider(
identities, keyCache);
+ FilePasswordProvider passwordProvider = getFilePasswordProvider();
ourConfiguredKeysProvider.setPasswordFinder(passwordProvider);
if (hostConfig.isIdentitiesOnly()) {
session.setKeyIdentityProvider(ourConfiguredKeysProvider);
@@ -265,9 +311,7 @@ public class JGitSshClient extends SshClient {
log.warn(format(SshdText.get().configInvalidPositive,
SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
}
- // Default for NumberOfPasswordPrompts according to
- // https://man.openbsd.org/ssh_config
- return 3;
+ return ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS;
}
/**
@@ -408,6 +452,5 @@ public class JGitSshClient extends SshClient {
};
}
-
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
index 5b48a8cf99..078e411f29 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -16,8 +16,12 @@ import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.sshd.client.ClientAuthenticationManager;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.session.SessionContext;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.transport.CredentialsProvider;
@@ -25,39 +29,61 @@ import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
/**
- * A bridge from sshd's {@link RepeatingFilePasswordProvider} to our
+ * A bridge from sshd's {@link FilePasswordProvider} to our per-session
* {@link KeyPasswordProvider} API.
*/
-public class PasswordProviderWrapper implements RepeatingFilePasswordProvider {
+public class PasswordProviderWrapper implements FilePasswordProvider {
- private final KeyPasswordProvider delegate;
+ private static final AttributeKey<PerSessionState> STATE = new AttributeKey<>();
- private Map<String, AtomicInteger> counts = new ConcurrentHashMap<>();
+ private static class PerSessionState {
+
+ Map<String, AtomicInteger> counts = new ConcurrentHashMap<>();
+
+ KeyPasswordProvider delegate;
- /**
- * @param delegate
- */
- public PasswordProviderWrapper(@NonNull KeyPasswordProvider delegate) {
- this.delegate = delegate;
}
- @Override
- public void setAttempts(int numberOfPasswordPrompts) {
- delegate.setAttempts(numberOfPasswordPrompts);
+ private final Supplier<KeyPasswordProvider> factory;
+
+ /**
+ * Creates a new {@link PasswordProviderWrapper}.
+ *
+ * @param factory
+ * to use to create per-session {@link KeyPasswordProvider}s
+ */
+ public PasswordProviderWrapper(
+ @NonNull Supplier<KeyPasswordProvider> factory) {
+ this.factory = factory;
}
- @Override
- public int getAttempts() {
- return delegate.getAttempts();
+ private PerSessionState getState(SessionContext context) {
+ PerSessionState state = context.getAttribute(STATE);
+ if (state == null) {
+ state = new PerSessionState();
+ state.delegate = factory.get();
+ Integer maxNumberOfAttempts = context
+ .getInteger(ClientAuthenticationManager.PASSWORD_PROMPTS);
+ if (maxNumberOfAttempts != null
+ && maxNumberOfAttempts.intValue() > 0) {
+ state.delegate.setAttempts(maxNumberOfAttempts.intValue());
+ } else {
+ state.delegate.setAttempts(
+ ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS);
+ }
+ context.setAttribute(STATE, state);
+ }
+ return state;
}
@Override
public String getPassword(SessionContext session, NamedResource resource,
int attemptIndex) throws IOException {
String key = resource.getName();
- int attempt = counts
+ PerSessionState state = getState(session);
+ int attempt = state.counts
.computeIfAbsent(key, k -> new AtomicInteger()).get();
- char[] passphrase = delegate.getPassphrase(toUri(key), attempt);
+ char[] passphrase = state.delegate.getPassphrase(toUri(key), attempt);
if (passphrase == null) {
return null;
}
@@ -74,18 +100,19 @@ public class PasswordProviderWrapper implements RepeatingFilePasswordProvider {
String password, Exception err)
throws IOException, GeneralSecurityException {
String key = resource.getName();
- AtomicInteger count = counts.get(key);
+ PerSessionState state = getState(session);
+ AtomicInteger count = state.counts.get(key);
int numberOfAttempts = count == null ? 0 : count.incrementAndGet();
ResourceDecodeResult result = null;
try {
- if (delegate.keyLoaded(toUri(key), numberOfAttempts, err)) {
+ if (state.delegate.keyLoaded(toUri(key), numberOfAttempts, err)) {
result = ResourceDecodeResult.RETRY;
} else {
result = ResourceDecodeResult.TERMINATE;
}
} finally {
if (result != ResourceDecodeResult.RETRY) {
- counts.remove(key);
+ state.counts.remove(key);
}
}
return result;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java
deleted file mode 100644
index 86f0fe7b60..0000000000
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> 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.sshd;
-
-import org.apache.sshd.common.config.keys.FilePasswordProvider;
-
-/**
- * A {@link FilePasswordProvider} augmented to support repeatedly asking for
- * passwords.
- *
- */
-public interface RepeatingFilePasswordProvider extends FilePasswordProvider {
-
- /**
- * Define the maximum number of attempts to get a password that should be
- * attempted for one identity resource through this provider.
- *
- * @param numberOfPasswordPrompts
- * number of times to ask for a password;
- * {@link IllegalArgumentException} may be thrown if <= 0
- */
- void setAttempts(int numberOfPasswordPrompts);
-
- /**
- * Gets the maximum number of attempts to get a password that should be
- * attempted for one identity resource through this provider.
- *
- * @return the maximum number of attempts to try, always >= 1.
- */
- default int getAttempts() {
- return 1;
- }
-
-}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
index f67170e407..13bb3ebe75 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
@@ -19,12 +19,16 @@ public final class SshdText extends TranslationBundle {
// @formatter:off
/***/ public String authenticationCanceled;
+ /***/ public String authenticationOnClosedSession;
/***/ public String closeListenerFailed;
/***/ public String configInvalidPath;
/***/ public String configInvalidPattern;
/***/ public String configInvalidPositive;
+ /***/ public String configInvalidProxyJump;
/***/ public String configNoKnownHostKeyAlgorithms;
/***/ public String configNoRemainingHostKeyAlgorithms;
+ /***/ public String configProxyJumpNotSsh;
+ /***/ public String configProxyJumpWithPath;
/***/ public String ftpCloseFailed;
/***/ public String gssapiFailure;
/***/ public String gssapiInitFailure;
@@ -57,12 +61,14 @@ public final class SshdText extends TranslationBundle {
/***/ public String knownHostsUnknownKeyType;
/***/ public String knownHostsUserAskCreationMsg;
/***/ public String knownHostsUserAskCreationPrompt;
+ /***/ public String loginDenied;
/***/ public String passwordPrompt;
/***/ public String proxyCannotAuthenticate;
/***/ public String proxyHttpFailure;
/***/ public String proxyHttpInvalidUserName;
/***/ public String proxyHttpUnexpectedReply;
/***/ public String proxyHttpUnspecifiedFailureReason;
+ /***/ public String proxyJumpAbort;
/***/ public String proxyPasswordPrompt;
/***/ public String proxySocksAuthenticationFailed;
/***/ public String proxySocksFailureForbidden;
@@ -87,9 +93,11 @@ public final class SshdText extends TranslationBundle {
/***/ public String serverIdTooLong;
/***/ public String serverIdWithNul;
/***/ public String sessionCloseFailed;
+ /***/ public String sessionWithoutUsername;
/***/ public String sshClosingDown;
/***/ public String sshCommandTimeout;
/***/ public String sshProcessStillRunning;
+ /***/ public String sshProxySessionCloseFailed;
/***/ public String unknownProxyProtocol;
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java
index d5b80374cb..0500a63428 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java
@@ -13,6 +13,8 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import org.eclipse.jgit.util.HttpSupport;
+
/**
* A basic parser for HTTP response headers. Handles status lines and
* authentication headers (WWW-Authenticate, Proxy-Authenticate).
@@ -135,7 +137,7 @@ public final class HttpParser {
int length = header.length();
for (int i = 0; i < length;) {
int start = skipWhiteSpace(header, i);
- int end = scanToken(header, start);
+ int end = HttpSupport.scanToken(header, start);
if (end <= start) {
break;
}
@@ -156,7 +158,7 @@ public final class HttpParser {
// optional legacy whitespace around the equals sign), where the
// value can be either a token or a quoted string.
start = skipWhiteSpace(header, start);
- int end = scanToken(header, start);
+ int end = HttpSupport.scanToken(header, start);
if (end == start) {
// Nothing found. Either at end or on a comma.
if (start < header.length() && header.charAt(start) == ',') {
@@ -222,7 +224,7 @@ public final class HttpParser {
challenge.addArgument(header.substring(start, end), value);
start = nextEnd[0];
} else {
- int nextEnd = scanToken(header, nextStart);
+ int nextEnd = HttpSupport.scanToken(header, nextStart);
challenge.addArgument(header.substring(start, end),
header.substring(nextStart, nextEnd));
start = nextEnd;
@@ -244,49 +246,6 @@ public final class HttpParser {
return i;
}
- private static int scanToken(String header, int from) {
- int length = header.length();
- int i = from;
- while (i < length) {
- char c = header.charAt(i);
- switch (c) {
- case '!':
- case '#':
- case '$':
- case '%':
- case '&':
- case '\'':
- case '*':
- case '+':
- case '-':
- case '.':
- case '^':
- case '_':
- case '`':
- case '|':
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- case '8':
- case '9':
- i++;
- break;
- default:
- if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
- i++;
- break;
- }
- return i;
- }
- }
- return i;
- }
-
private static String scanQuotedString(String header, int from, int[] to) {
StringBuilder result = new StringBuilder();
int length = header.length();
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
index 004d3f8361..dd6894b662 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
@@ -19,13 +19,14 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.CancellationException;
import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.StringUtils;
/**
* A {@link KeyPasswordProvider} based on a {@link CredentialsProvider}.
@@ -155,35 +156,84 @@ public class IdentityPasswordProvider implements KeyPasswordProvider {
state.incCount();
String message = state.count == 1 ? SshdText.get().keyEncryptedMsg
: SshdText.get().keyEncryptedRetry;
- char[] pass = getPassword(uri, message);
+ char[] pass = getPassword(uri, format(message, uri));
state.setPassword(pass);
return pass;
}
- private char[] getPassword(URIish uri, String message) {
+ /**
+ * Retrieves the JGit {@link CredentialsProvider} to use for user
+ * interaction.
+ *
+ * @return the {@link CredentialsProvider} or {@code null} if none
+ * configured
+ * @since 5.10
+ */
+ protected CredentialsProvider getCredentialsProvider() {
+ return provider;
+ }
+
+ /**
+ * Obtains the passphrase/password for an encrypted private key via the
+ * {@link #getCredentialsProvider() configured CredentialsProvider}.
+ *
+ * @param uri
+ * identifying the resource to obtain a password for
+ * @param message
+ * optional message text to display; may be {@code null} or empty
+ * if none
+ * @return the password entered, or {@code null} if no
+ * {@link CredentialsProvider} is configured or none was entered
+ * @throws java.util.concurrent.CancellationException
+ * if the user canceled the operation
+ * @since 5.10
+ */
+ protected char[] getPassword(URIish uri, String message) {
if (provider == null) {
return null;
}
- List<CredentialItem> items = new ArrayList<>(2);
- items.add(new CredentialItem.InformationalMessage(
- format(message, uri)));
+ boolean haveMessage = !StringUtils.isEmptyOrNull(message);
+ List<CredentialItem> items = new ArrayList<>(haveMessage ? 2 : 1);
+ if (haveMessage) {
+ items.add(new CredentialItem.InformationalMessage(message));
+ }
CredentialItem.Password password = new CredentialItem.Password(
SshdText.get().keyEncryptedPrompt);
items.add(password);
try {
- provider.get(uri, items);
+ boolean completed = provider.get(uri, items);
char[] pass = password.getValue();
- if (pass == null) {
- throw new CancellationException(
- SshdText.get().authenticationCanceled);
+ if (!completed) {
+ cancelAuthentication();
+ return null;
}
- return pass.clone();
+ return pass == null ? null : pass.clone();
} finally {
password.clear();
}
}
/**
+ * Cancels the authentication process. Called by
+ * {@link #getPassword(URIish, String)} when the user interaction has been
+ * canceled. If this throws a
+ * {@link java.util.concurrent.CancellationException}, the authentication
+ * process is aborted; otherwise it may continue with the next configured
+ * authentication mechanism, if any.
+ * <p>
+ * This default implementation always throws a
+ * {@link java.util.concurrent.CancellationException}.
+ * </p>
+ *
+ * @throws java.util.concurrent.CancellationException
+ * always
+ * @since 5.10
+ */
+ protected void cancelAuthentication() {
+ throw new AuthenticationCanceledException();
+ }
+
+ /**
* Invoked to inform the password provider about the decoding result.
*
* @param uri
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
index 0c1533c614..0fb0610b99 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -10,37 +10,53 @@
package org.eclipse.jgit.transport.sshd;
import static java.text.MessageFormat.format;
+import static org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InterruptedIOException;
import java.io.OutputStream;
+import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.EnumSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.regex.Pattern;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.forward.PortForwardingTracker;
import org.apache.sshd.client.subsystem.sftp.SftpClient;
import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.FtpChannel;
import org.eclipse.jgit.transport.RemoteSession;
+import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,6 +70,11 @@ public class SshdSession implements RemoteSession {
private static final Logger LOG = LoggerFactory
.getLogger(SshdSession.class);
+ private static final Pattern SHORT_SSH_FORMAT = Pattern
+ .compile("[-\\w.]+(?:@[-\\w.]+)?(?::\\d+)?"); //$NON-NLS-1$
+
+ private static final int MAX_DEPTH = 10;
+
private final CopyOnWriteArrayList<SessionCloseListener> listeners = new CopyOnWriteArrayList<>();
private final URIish uri;
@@ -72,32 +93,169 @@ public class SshdSession implements RemoteSession {
client.start();
}
try {
- String username = uri.getUser();
- String host = uri.getHost();
- int port = uri.getPort();
- long t = timeout.toMillis();
- if (t <= 0) {
- session = client.connect(username, host, port).verify()
- .getSession();
- } else {
- session = client.connect(username, host, port)
- .verify(timeout.toMillis()).getSession();
- }
- session.addSessionListener(new SessionListener() {
+ session = connect(uri, Collections.emptyList(),
+ future -> notifyCloseListeners(), timeout, MAX_DEPTH);
+ } catch (IOException e) {
+ disconnect(e);
+ throw e;
+ }
+ }
- @Override
- public void sessionClosed(Session s) {
- notifyCloseListeners();
+ private ClientSession connect(URIish target, List<URIish> jumps,
+ SshFutureListener<CloseFuture> listener, Duration timeout,
+ int depth) throws IOException {
+ if (--depth < 0) {
+ throw new IOException(
+ format(SshdText.get().proxyJumpAbort, target));
+ }
+ HostConfigEntry hostConfig = getHostConfig(target.getUser(),
+ target.getHost(), target.getPort());
+ String host = hostConfig.getHostName();
+ int port = hostConfig.getPort();
+ List<URIish> hops = determineHops(jumps, hostConfig, target.getHost());
+ ClientSession resultSession = null;
+ ClientSession proxySession = null;
+ PortForwardingTracker portForward = null;
+ try {
+ if (!hops.isEmpty()) {
+ URIish hop = hops.remove(0);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Connecting to jump host {}", hop); //$NON-NLS-1$
}
- });
+ proxySession = connect(hop, hops, null, timeout, depth);
+ }
+ AttributeRepository context = null;
+ if (proxySession != null) {
+ SshdSocketAddress remoteAddress = new SshdSocketAddress(host,
+ port);
+ portForward = proxySession.createLocalPortForwardingTracker(
+ SshdSocketAddress.LOCALHOST_ADDRESS, remoteAddress);
+ // We must connect to the locally bound address, not the one
+ // from the host config.
+ context = AttributeRepository.ofKeyValuePair(
+ JGitSshClient.LOCAL_FORWARD_ADDRESS,
+ portForward.getBoundAddress());
+ }
+ resultSession = connect(hostConfig, context, timeout);
+ if (proxySession != null) {
+ final PortForwardingTracker tracker = portForward;
+ final ClientSession pSession = proxySession;
+ resultSession.addCloseFutureListener(future -> {
+ IoUtils.closeQuietly(tracker);
+ String sessionName = pSession.toString();
+ try {
+ pSession.close();
+ } catch (IOException e) {
+ LOG.error(format(
+ SshdText.get().sshProxySessionCloseFailed,
+ sessionName), e);
+ }
+ });
+ portForward = null;
+ proxySession = null;
+ }
+ if (listener != null) {
+ resultSession.addCloseFutureListener(listener);
+ }
// Authentication timeout is by default 2 minutes.
- session.auth().verify(session.getAuthTimeout());
+ resultSession.auth().verify(resultSession.getAuthTimeout());
+ return resultSession;
} catch (IOException e) {
- disconnect(e);
+ close(portForward, e);
+ close(proxySession, e);
+ close(resultSession, e);
+ if (e instanceof SshException && ((SshException) e)
+ .getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) {
+ // Ensure the user gets to know on which URI the authentication
+ // was denied.
+ throw new TransportException(target,
+ format(SshdText.get().loginDenied, host,
+ Integer.toString(port)),
+ e);
+ }
throw e;
}
}
+ private ClientSession connect(HostConfigEntry config,
+ AttributeRepository context, Duration timeout)
+ throws IOException {
+ ConnectFuture connected = client.connect(config, context, null);
+ long timeoutMillis = timeout.toMillis();
+ if (timeoutMillis <= 0) {
+ connected = connected.verify();
+ } else {
+ connected = connected.verify(timeoutMillis);
+ }
+ return connected.getSession();
+ }
+
+ private void close(Closeable toClose, Throwable error) {
+ if (toClose != null) {
+ try {
+ toClose.close();
+ } catch (IOException e) {
+ error.addSuppressed(e);
+ }
+ }
+ }
+
+ private HostConfigEntry getHostConfig(String username, String host,
+ int port) throws IOException {
+ HostConfigEntry entry = client.getHostConfigEntryResolver()
+ .resolveEffectiveHost(host, port, null, username, null);
+ if (entry == null) {
+ if (SshdSocketAddress.isIPv6Address(host)) {
+ return new HostConfigEntry("", host, port, username); //$NON-NLS-1$
+ }
+ return new HostConfigEntry(host, host, port, username);
+ }
+ return entry;
+ }
+
+ private List<URIish> determineHops(List<URIish> currentHops,
+ HostConfigEntry hostConfig, String host) throws IOException {
+ if (currentHops.isEmpty()) {
+ String jumpHosts = hostConfig.getProperty(SshConstants.PROXY_JUMP);
+ if (!StringUtils.isEmptyOrNull(jumpHosts)) {
+ try {
+ return parseProxyJump(jumpHosts);
+ } catch (URISyntaxException e) {
+ throw new IOException(
+ format(SshdText.get().configInvalidProxyJump, host,
+ jumpHosts),
+ e);
+ }
+ }
+ }
+ return currentHops;
+ }
+
+ private List<URIish> parseProxyJump(String proxyJump)
+ throws URISyntaxException {
+ String[] hops = proxyJump.split(","); //$NON-NLS-1$
+ List<URIish> result = new LinkedList<>();
+ for (String hop : hops) {
+ // There shouldn't be any whitespace, but let's be lenient
+ hop = hop.trim();
+ if (SHORT_SSH_FORMAT.matcher(hop).matches()) {
+ // URIish doesn't understand the short SSH format
+ // user@host:port, only user@host:path
+ hop = SshConstants.SSH_SCHEME + "://" + hop; //$NON-NLS-1$
+ }
+ URIish to = new URIish(hop);
+ if (!SshConstants.SSH_SCHEME.equalsIgnoreCase(to.getScheme())) {
+ throw new URISyntaxException(hop,
+ SshdText.get().configProxyJumpNotSsh);
+ } else if (!StringUtils.isEmptyOrNull(to.getPath())) {
+ throw new URISyntaxException(hop,
+ SshdText.get().configProxyJumpWithPath);
+ }
+ result.add(to);
+ }
+ return result;
+ }
+
/**
* Adds a {@link SessionCloseListener} to this session. Has no effect if the
* given {@code listener} is already registered with this session.
@@ -134,28 +292,23 @@ public class SshdSession implements RemoteSession {
public Process exec(String commandName, int timeout) throws IOException {
@SuppressWarnings("resource")
ChannelExec exec = session.createExecChannel(commandName);
- long timeoutMillis = TimeUnit.SECONDS.toMillis(timeout);
- try {
- if (timeout <= 0) {
+ if (timeout <= 0) {
+ try {
exec.open().verify();
- } else {
- long start = System.nanoTime();
- exec.open().verify(timeoutMillis);
- timeoutMillis -= TimeUnit.NANOSECONDS
- .toMillis(System.nanoTime() - start);
+ } catch (IOException | RuntimeException e) {
+ exec.close(true);
+ throw e;
+ }
+ } else {
+ try {
+ exec.open().verify(TimeUnit.SECONDS.toMillis(timeout));
+ } catch (IOException | RuntimeException e) {
+ exec.close(true);
+ throw new IOException(format(SshdText.get().sshCommandTimeout,
+ commandName, Integer.valueOf(timeout)), e);
}
- } catch (IOException | RuntimeException e) {
- exec.close(true);
- throw e;
- }
- if (timeout > 0 && timeoutMillis <= 0) {
- // We have used up the whole timeout for opening the channel
- exec.close(true);
- throw new InterruptedIOException(
- format(SshdText.get().sshCommandTimeout, commandName,
- Integer.valueOf(timeout)));
}
- return new SshdExecProcess(exec, commandName, timeoutMillis);
+ return new SshdExecProcess(exec, commandName);
}
/**
@@ -195,14 +348,10 @@ public class SshdSession implements RemoteSession {
private final ChannelExec channel;
- private final long timeoutMillis;
-
private final String commandName;
- public SshdExecProcess(ChannelExec channel, String commandName,
- long timeoutMillis) {
+ public SshdExecProcess(ChannelExec channel, String commandName) {
this.channel = channel;
- this.timeoutMillis = timeoutMillis > 0 ? timeoutMillis : -1L;
this.commandName = commandName;
}
@@ -223,7 +372,7 @@ public class SshdSession implements RemoteSession {
@Override
public int waitFor() throws InterruptedException {
- if (waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) {
+ if (waitFor(-1L, TimeUnit.MILLISECONDS)) {
return exitValue();
}
return -1;
@@ -252,7 +401,7 @@ public class SshdSession implements RemoteSession {
@Override
public void destroy() {
if (channel.isOpen()) {
- channel.close(true);
+ channel.close(false);
}
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
index bb4e49be8e..df0e1d28a4 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.sshd.client.ClientBuilder;
@@ -33,6 +34,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
+import org.apache.sshd.common.SshException;
import org.apache.sshd.common.compression.BuiltinCompressions;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
@@ -40,6 +42,7 @@ import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
@@ -194,12 +197,11 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
home, sshDir);
KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
getDefaultKeys(sshDir));
- KeyPasswordProvider passphrases = createKeyPasswordProvider(
- credentialsProvider);
SshClient client = ClientBuilder.builder()
.factory(JGitSshClient::new)
- .filePasswordProvider(
- createFilePasswordProvider(passphrases))
+ .filePasswordProvider(createFilePasswordProvider(
+ () -> createKeyPasswordProvider(
+ credentialsProvider)))
.hostConfigEntryResolver(configFile)
.serverKeyVerifier(new JGitServerKeyVerifier(
getServerKeyDatabase(home, sshDir)))
@@ -230,7 +232,16 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
return session;
} catch (Exception e) {
unregister(session);
- throw new TransportException(uri, e.getMessage(), e);
+ if (e instanceof TransportException) {
+ throw (TransportException) e;
+ }
+ Throwable cause = e;
+ if (e instanceof SshException && e
+ .getCause() instanceof AuthenticationCanceledException) {
+ // Results in a nicer error message
+ cause = e.getCause();
+ }
+ throw new TransportException(uri, cause.getMessage(), cause);
}
}
@@ -536,14 +547,14 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
/**
* Creates a {@link FilePasswordProvider} for a new session.
*
- * @param provider
- * the {@link KeyPasswordProvider} to delegate to
+ * @param providerFactory
+ * providing the {@link KeyPasswordProvider} to delegate to
* @return a new {@link FilePasswordProvider}
*/
@NonNull
private FilePasswordProvider createFilePasswordProvider(
- KeyPasswordProvider provider) {
- return new PasswordProviderWrapper(provider);
+ Supplier<KeyPasswordProvider> providerFactory) {
+ return new PasswordProviderWrapper(providerFactory);
}
/**

Back to the top