diff options
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src')
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); } /** |