* stable-5.11: Refactor CommitCommand to improve readability CommitCommand: fix formatting CommitCommand: remove unncessary comment Ensure post-commit hook is called after index lock was released sshd: try all configured signature algorithms for a key sshd: modernize ssh config file parsing sshd: implement ssh config PubkeyAcceptedAlgorithms Change-Id: Ic3235ffd84c9d7537a1fe5ff4f216578e6e26724 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>tags/v5.12.0.202105051250-m2
@@ -14,6 +14,7 @@ Import-Package: org.apache.sshd.client.config.hosts;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.helpers;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.keyprovider;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.session;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.signature;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.util.net;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.common.util.security;version="[2.6.0,2.7.0)", | |||
org.apache.sshd.core;version="[2.6.0,2.7.0)", |
@@ -3,3 +3,5 @@ output.. = bin/ | |||
bin.includes = META-INF/,\ | |||
.,\ | |||
plugin.properties | |||
additional.bundles = org.apache.log4j,\ | |||
org.slf4j.binding.log4j12 |
@@ -47,7 +47,9 @@ import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.api.errors.TransportException; | |||
import org.eclipse.jgit.junit.ssh.SshTestBase; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.transport.RemoteSession; | |||
import org.eclipse.jgit.transport.SshSessionFactory; | |||
import org.eclipse.jgit.transport.URIish; | |||
import org.eclipse.jgit.util.FS; | |||
import org.junit.Test; | |||
import org.junit.experimental.theories.Theories; | |||
@@ -232,64 +234,89 @@ public class ApacheSshTest extends SshTestBase { | |||
} | |||
/** | |||
* Creates a simple proxy server. Accepts only publickey authentication from | |||
* the given user with the given key, allows all forwardings. Adds the | |||
* proxy's host key to {@link #knownHosts}. | |||
* Creates a simple SSH server without git setup. | |||
* | |||
* @param user | |||
* to accept | |||
* @param userKey | |||
* public key of that user at this server | |||
* @param report | |||
* single-element array to report back the forwarded address. | |||
* @return the started server | |||
* @return the {@link SshServer}, not yet started | |||
* @throws Exception | |||
*/ | |||
private SshServer createProxy(String user, File userKey, | |||
SshdSocketAddress[] report) throws Exception { | |||
SshServer proxy = SshServer.setUpDefaultServer(); | |||
private SshServer createServer(String user, File userKey) throws Exception { | |||
SshServer srv = SshServer.setUpDefaultServer(); | |||
// Give the server its own host key | |||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); | |||
generator.initialize(2048); | |||
KeyPair proxyHostKey = generator.generateKeyPair(); | |||
proxy.setKeyPairProvider( | |||
srv.setKeyPairProvider( | |||
session -> Collections.singletonList(proxyHostKey)); | |||
// Allow (only) publickey authentication | |||
proxy.setUserAuthFactories(Collections.singletonList( | |||
srv.setUserAuthFactories(Collections.singletonList( | |||
ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY)); | |||
// Install the user's public key | |||
PublicKey userProxyKey = AuthorizedKeyEntry | |||
.readAuthorizedKeys(userKey.toPath()).get(0) | |||
.resolvePublicKey(null, PublicKeyEntryResolver.IGNORING); | |||
proxy.setPublickeyAuthenticator( | |||
srv.setPublickeyAuthenticator( | |||
(userName, publicKey, session) -> user.equals(userName) | |||
&& KeyUtils.compareKeys(userProxyKey, publicKey)); | |||
// Allow forwarding | |||
proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) { | |||
return srv; | |||
} | |||
@Override | |||
protected boolean checkAcceptance(String request, Session session, | |||
SshdSocketAddress target) { | |||
report[0] = target; | |||
return super.checkAcceptance(request, session, target); | |||
} | |||
}); | |||
proxy.start(); | |||
/** | |||
* Writes the server's host key to our knownhosts file. | |||
* | |||
* @param srv to register | |||
* @throws Exception | |||
*/ | |||
private void registerServer(SshServer srv) throws Exception { | |||
// Add the proxy's host key to knownhosts | |||
try (BufferedWriter writer = Files.newBufferedWriter( | |||
knownHosts.toPath(), StandardCharsets.US_ASCII, | |||
StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { | |||
writer.append('\n'); | |||
KnownHostHashValue.appendHostPattern(writer, "localhost", | |||
proxy.getPort()); | |||
srv.getPort()); | |||
writer.append(','); | |||
KnownHostHashValue.appendHostPattern(writer, "127.0.0.1", | |||
proxy.getPort()); | |||
srv.getPort()); | |||
writer.append(' '); | |||
PublicKeyEntry.appendPublicKeyEntry(writer, | |||
proxyHostKey.getPublic()); | |||
srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic()); | |||
writer.append('\n'); | |||
} | |||
} | |||
/** | |||
* Creates a simple proxy server. Accepts only publickey authentication from | |||
* the given user with the given key, allows all forwardings. Adds the | |||
* proxy's host key to {@link #knownHosts}. | |||
* | |||
* @param user | |||
* to accept | |||
* @param userKey | |||
* public key of that user at this server | |||
* @param report | |||
* single-element array to report back the forwarded address. | |||
* @return the started server | |||
* @throws Exception | |||
*/ | |||
private SshServer createProxy(String user, File userKey, | |||
SshdSocketAddress[] report) throws Exception { | |||
SshServer proxy = createServer(user, userKey); | |||
// Allow forwarding | |||
proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) { | |||
@Override | |||
protected boolean checkAcceptance(String request, Session session, | |||
SshdSocketAddress target) { | |||
report[0] = target; | |||
return super.checkAcceptance(request, session, target); | |||
} | |||
}); | |||
proxy.start(); | |||
registerServer(proxy); | |||
return proxy; | |||
} | |||
@@ -606,4 +633,73 @@ public class ApacheSshTest extends SshTestBase { | |||
} | |||
} | |||
} | |||
/** | |||
* Tests that one can log in to an old server that doesn't handle | |||
* rsa-sha2-512 if one puts ssh-rsa first in the client's list of public key | |||
* signature algorithms. | |||
* | |||
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug | |||
* 572056</a> | |||
* @throws Exception | |||
* on failure | |||
*/ | |||
@Test | |||
public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms() | |||
throws Exception { | |||
try (SshServer oldServer = createServer(TEST_USER, publicKey1)) { | |||
oldServer.setSignatureFactoriesNames("ssh-rsa"); | |||
oldServer.start(); | |||
registerServer(oldServer); | |||
installConfig("Host server", // | |||
"HostName localhost", // | |||
"Port " + oldServer.getPort(), // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), // | |||
"PubkeyAcceptedAlgorithms ^ssh-rsa"); | |||
RemoteSession session = getSessionFactory().getSession( | |||
new URIish("ssh://server/doesntmatter"), null, FS.DETECTED, | |||
10000); | |||
assertNotNull(session); | |||
session.disconnect(); | |||
} | |||
} | |||
/** | |||
* Tests that one can log in to an old server that knows only the ssh-rsa | |||
* signature algorithm. The client has by default the list of signature | |||
* algorithms for RSA as "rsa-sha2-512,rsa-sha2-256,ssh-rsa". It should try | |||
* all three with the single key configured, and finally succeed. | |||
* <p> | |||
* The re-ordering mechanism (see | |||
* {@link #testConnectAuthSshRsaPubkeyAcceptedAlgorithms()}) is still | |||
* important; servers may impose a penalty (back-off delay) for subsequent | |||
* attempts with signature algorithms unknown to the server. So a user | |||
* connecting to such a server and noticing delays may still want to put | |||
* ssh-rsa first in the list for that host. | |||
* </p> | |||
* | |||
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug | |||
* 572056</a> | |||
* @throws Exception | |||
* on failure | |||
*/ | |||
@Test | |||
public void testConnectAuthSshRsa() throws Exception { | |||
try (SshServer oldServer = createServer(TEST_USER, publicKey1)) { | |||
oldServer.setSignatureFactoriesNames("ssh-rsa"); | |||
oldServer.start(); | |||
registerServer(oldServer); | |||
installConfig("Host server", // | |||
"HostName localhost", // | |||
"Port " + oldServer.getPort(), // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
RemoteSession session = getSessionFactory().getSession( | |||
new URIish("ssh://server/doesntmatter"), null, FS.DETECTED, | |||
10000); | |||
assertNotNull(session); | |||
session.disconnect(); | |||
} | |||
} | |||
} |
@@ -5,8 +5,7 @@ configInvalidPath=Invalid path in ssh config key {0}: {1} | |||
configInvalidPattern=Invalid pattern in ssh config key {0}: {1} | |||
configInvalidPositive=Ssh config entry {0} must be a strictly positive number but is ''{1}'' | |||
configInvalidProxyJump=Ssh config, host ''{0}'': Cannot parse ProxyJump ''{1}'' | |||
configNoKnownHostKeyAlgorithms=No implementations for any of the algorithms ''{0}'' given in HostKeyAlgorithms in the ssh config; using the default. | |||
configNoRemainingHostKeyAlgorithms=Ssh config removed all host key algorithms: HostKeyAlgorithms ''{0}'' | |||
configNoKnownAlgorithms=Ssh config ''{0}'' ''{1}'' resulted in empty list (none known, or all known removed); using default. | |||
configProxyJumpNotSsh=Non-ssh URI in ProxyJump ssh config | |||
configProxyJumpWithPath=ProxyJump ssh config: jump host specification must not have a path | |||
ftpCloseFailed=Closing the SFTP channel failed |
@@ -21,6 +21,7 @@ import java.security.PublicKey; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.HashSet; | |||
import java.util.Iterator; | |||
import java.util.LinkedHashSet; | |||
import java.util.List; | |||
@@ -45,6 +46,7 @@ import org.eclipse.jgit.fnmatch.FileNameMatcher; | |||
import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector; | |||
import org.eclipse.jgit.transport.CredentialsProvider; | |||
import org.eclipse.jgit.transport.SshConstants; | |||
import org.eclipse.jgit.util.StringUtils; | |||
/** | |||
* A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can | |||
@@ -201,48 +203,23 @@ public class JGitClientSession extends ClientSessionImpl { | |||
@Override | |||
protected String resolveAvailableSignaturesProposal( | |||
FactoryManager manager) { | |||
Set<String> defaultSignatures = new LinkedHashSet<>(); | |||
defaultSignatures.addAll(getSignatureFactoriesNames()); | |||
List<String> defaultSignatures = getSignatureFactoriesNames(); | |||
HostConfigEntry config = resolveAttribute( | |||
JGitSshClient.HOST_CONFIG_ENTRY); | |||
String hostKeyAlgorithms = config | |||
String algorithms = config | |||
.getProperty(SshConstants.HOST_KEY_ALGORITHMS); | |||
if (hostKeyAlgorithms != null && !hostKeyAlgorithms.isEmpty()) { | |||
char first = hostKeyAlgorithms.charAt(0); | |||
switch (first) { | |||
case '+': | |||
// Additions make not much sense -- it's either in | |||
// defaultSignatures already, or we have no implementation for | |||
// it. No point in proposing it. | |||
return String.join(",", defaultSignatures); //$NON-NLS-1$ | |||
case '-': | |||
// This takes wildcard patterns! | |||
removeFromList(defaultSignatures, | |||
SshConstants.HOST_KEY_ALGORITHMS, | |||
hostKeyAlgorithms.substring(1)); | |||
if (defaultSignatures.isEmpty()) { | |||
// Too bad: user config error. Warn here, and then fail | |||
// later. | |||
log.warn(format( | |||
SshdText.get().configNoRemainingHostKeyAlgorithms, | |||
hostKeyAlgorithms)); | |||
} | |||
return String.join(",", defaultSignatures); //$NON-NLS-1$ | |||
default: | |||
// Default is overridden -- only accept the ones for which we do | |||
// have an implementation. | |||
List<String> newNames = filteredList(defaultSignatures, | |||
hostKeyAlgorithms); | |||
if (newNames.isEmpty()) { | |||
log.warn(format( | |||
SshdText.get().configNoKnownHostKeyAlgorithms, | |||
hostKeyAlgorithms)); | |||
// Use the default instead. | |||
} else { | |||
return String.join(",", newNames); //$NON-NLS-1$ | |||
if (!StringUtils.isEmptyOrNull(algorithms)) { | |||
List<String> result = modifyAlgorithmList(defaultSignatures, | |||
algorithms, SshConstants.HOST_KEY_ALGORITHMS); | |||
if (!result.isEmpty()) { | |||
if (log.isDebugEnabled()) { | |||
log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + result); | |||
} | |||
break; | |||
return String.join(",", result); //$NON-NLS-1$ | |||
} | |||
log.warn(format(SshdText.get().configNoKnownAlgorithms, | |||
SshConstants.HOST_KEY_ALGORITHMS, | |||
algorithms)); | |||
} | |||
// No HostKeyAlgorithms; using default -- change order to put existing | |||
// keys first. | |||
@@ -262,11 +239,67 @@ public class JGitClientSession extends ClientSessionImpl { | |||
} | |||
} | |||
reordered.addAll(defaultSignatures); | |||
if (log.isDebugEnabled()) { | |||
log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + reordered); | |||
} | |||
return String.join(",", reordered); //$NON-NLS-1$ | |||
} | |||
if (log.isDebugEnabled()) { | |||
log.debug( | |||
SshConstants.HOST_KEY_ALGORITHMS + ' ' + defaultSignatures); | |||
} | |||
return String.join(",", defaultSignatures); //$NON-NLS-1$ | |||
} | |||
/** | |||
* Modifies a given algorithm list according to a list from the ssh config, | |||
* including remove ('-') and reordering ('^') operators. Addition ('+') is | |||
* not handled since we have no way of adding dynamically implementations, | |||
* and the defaultList is supposed to contain all known implementations | |||
* already. | |||
* | |||
* @param defaultList | |||
* to modify | |||
* @param fromConfig | |||
* telling how to modify the {@code defaultList}, must not be | |||
* {@code null} or empty | |||
* @param overrideKey | |||
* ssh config key; used for logging | |||
* @return the modified list or {@code null} if {@code overrideKey} is not | |||
* set | |||
*/ | |||
public List<String> modifyAlgorithmList(List<String> defaultList, | |||
String fromConfig, String overrideKey) { | |||
Set<String> defaults = new LinkedHashSet<>(); | |||
defaults.addAll(defaultList); | |||
switch (fromConfig.charAt(0)) { | |||
case '+': | |||
// Additions make not much sense -- it's either in | |||
// defaultList already, or we have no implementation for | |||
// it. No point in proposing it. | |||
return defaultList; | |||
case '-': | |||
// This takes wildcard patterns! | |||
removeFromList(defaults, overrideKey, fromConfig.substring(1)); | |||
return new ArrayList<>(defaults); | |||
case '^': | |||
// Specified entries go to the front of the default list | |||
List<String> allSignatures = filteredList(defaults, | |||
fromConfig.substring(1)); | |||
Set<String> atFront = new HashSet<>(allSignatures); | |||
for (String sig : defaults) { | |||
if (!atFront.contains(sig)) { | |||
allSignatures.add(sig); | |||
} | |||
} | |||
return allSignatures; | |||
default: | |||
// Default is overridden -- only accept the ones for which we do | |||
// have an implementation. | |||
return filteredList(defaults, fromConfig); | |||
} | |||
} | |||
private void removeFromList(Set<String> current, String key, | |||
String patterns) { | |||
for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$ |
@@ -0,0 +1,35 @@ | |||
/* | |||
* Copyright (C) 2018, 2021 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.io.IOException; | |||
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; | |||
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory; | |||
import org.apache.sshd.client.session.ClientSession; | |||
/** | |||
* A customized authentication factory for public key user authentication. | |||
*/ | |||
public class JGitPublicKeyAuthFactory extends UserAuthPublicKeyFactory { | |||
/** The singleton {@link JGitPublicKeyAuthFactory}. */ | |||
public static final JGitPublicKeyAuthFactory FACTORY = new JGitPublicKeyAuthFactory(); | |||
private JGitPublicKeyAuthFactory() { | |||
super(); | |||
} | |||
@Override | |||
public UserAuthPublicKey createUserAuth(ClientSession session) | |||
throws IOException { | |||
return new JGitPublicKeyAuthentication(getSignatureFactories()); | |||
} | |||
} |
@@ -0,0 +1,133 @@ | |||
/* | |||
* Copyright (C) 2018, 2021 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.io.IOException; | |||
import java.security.PublicKey; | |||
import java.util.HashSet; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Set; | |||
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; | |||
import org.apache.sshd.client.session.ClientSession; | |||
import org.apache.sshd.common.NamedFactory; | |||
import org.apache.sshd.common.RuntimeSshException; | |||
import org.apache.sshd.common.SshConstants; | |||
import org.apache.sshd.common.config.keys.KeyUtils; | |||
import org.apache.sshd.common.signature.Signature; | |||
import org.apache.sshd.common.signature.SignatureFactoriesHolder; | |||
import org.apache.sshd.common.util.buffer.Buffer; | |||
/** | |||
* Custom {@link UserAuthPublicKey} implementation fixing SSHD-1105: if there | |||
* are several signature algorithms applicable for a public key type, we must | |||
* try them all, in the correct order. | |||
* | |||
* @see <a href="https://issues.apache.org/jira/browse/SSHD-1105">SSHD-1105</a> | |||
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">Bug | |||
* 572056</a> | |||
*/ | |||
public class JGitPublicKeyAuthentication extends UserAuthPublicKey { | |||
private final List<String> algorithms = new LinkedList<>(); | |||
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) { | |||
super(factories); | |||
} | |||
@Override | |||
protected boolean sendAuthDataRequest(ClientSession session, String service) | |||
throws Exception { | |||
if (current == null) { | |||
algorithms.clear(); | |||
} | |||
String currentAlgorithm = null; | |||
if (current != null && !algorithms.isEmpty()) { | |||
currentAlgorithm = algorithms.remove(0); | |||
} | |||
if (currentAlgorithm == null) { | |||
try { | |||
if (keys == null || !keys.hasNext()) { | |||
if (log.isDebugEnabled()) { | |||
log.debug( | |||
"sendAuthDataRequest({})[{}] no more keys to send", //$NON-NLS-1$ | |||
session, service); | |||
} | |||
return false; | |||
} | |||
current = keys.next(); | |||
algorithms.clear(); | |||
} catch (Error e) { // Copied from superclass | |||
warn("sendAuthDataRequest({})[{}] failed ({}) to get next key: {}", //$NON-NLS-1$ | |||
session, service, e.getClass().getSimpleName(), | |||
e.getMessage(), e); | |||
throw new RuntimeSshException(e); | |||
} | |||
} | |||
PublicKey key; | |||
try { | |||
key = current.getPublicKey(); | |||
} catch (Error e) { // Copied from superclass | |||
warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve public key: {}", //$NON-NLS-1$ | |||
session, service, e.getClass().getSimpleName(), | |||
e.getMessage(), e); | |||
throw new RuntimeSshException(e); | |||
} | |||
if (currentAlgorithm == null) { | |||
String keyType = KeyUtils.getKeyType(key); | |||
Set<String> aliases = new HashSet<>( | |||
KeyUtils.getAllEquivalentKeyTypes(keyType)); | |||
aliases.add(keyType); | |||
List<NamedFactory<Signature>> existingFactories; | |||
if (current instanceof SignatureFactoriesHolder) { | |||
existingFactories = ((SignatureFactoriesHolder) current) | |||
.getSignatureFactories(); | |||
} else { | |||
existingFactories = getSignatureFactories(); | |||
} | |||
if (existingFactories != null) { | |||
// Select the factories by name and in order | |||
existingFactories.forEach(f -> { | |||
if (aliases.contains(f.getName())) { | |||
algorithms.add(f.getName()); | |||
} | |||
}); | |||
} | |||
currentAlgorithm = algorithms.isEmpty() ? keyType | |||
: algorithms.remove(0); | |||
} | |||
String name = getName(); | |||
if (log.isDebugEnabled()) { | |||
log.debug( | |||
"sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST request {} type={} - fingerprint={}", //$NON-NLS-1$ | |||
session, service, name, currentAlgorithm, | |||
KeyUtils.getFingerPrint(key)); | |||
} | |||
Buffer buffer = session | |||
.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST); | |||
buffer.putString(session.getUsername()); | |||
buffer.putString(service); | |||
buffer.putString(name); | |||
buffer.putBoolean(false); | |||
buffer.putString(currentAlgorithm); | |||
buffer.putPublicKey(key); | |||
session.writePacket(buffer); | |||
return true; | |||
} | |||
@Override | |||
protected void releaseKeys() throws IOException { | |||
algorithms.clear(); | |||
current = null; | |||
super.releaseKeys(); | |||
} | |||
} |
@@ -267,6 +267,24 @@ public class JGitSshClient extends SshClient { | |||
session.setUsername(username); | |||
session.setConnectAddress(address); | |||
session.setHostConfigEntry(hostConfig); | |||
// Set signature algorithms for public key authentication | |||
String pubkeyAlgos = hostConfig | |||
.getProperty(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS); | |||
if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) { | |||
List<String> signatures = getSignatureFactoriesNames(); | |||
signatures = session.modifyAlgorithmList(signatures, pubkeyAlgos, | |||
SshConstants.PUBKEY_ACCEPTED_ALGORITHMS); | |||
if (!signatures.isEmpty()) { | |||
if (log.isDebugEnabled()) { | |||
log.debug(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS + ' ' | |||
+ signatures); | |||
} | |||
session.setSignatureFactoriesNames(signatures); | |||
} else { | |||
log.warn(format(SshdText.get().configNoKnownAlgorithms, | |||
SshConstants.PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos)); | |||
} | |||
} | |||
if (session.getCredentialsProvider() == null) { | |||
session.setCredentialsProvider(getCredentialsProvider()); | |||
} |
@@ -25,8 +25,7 @@ public final class SshdText extends TranslationBundle { | |||
/***/ public String configInvalidPattern; | |||
/***/ public String configInvalidPositive; | |||
/***/ public String configInvalidProxyJump; | |||
/***/ public String configNoKnownHostKeyAlgorithms; | |||
/***/ public String configNoRemainingHostKeyAlgorithms; | |||
/***/ public String configNoKnownAlgorithms; | |||
/***/ public String configProxyJumpNotSsh; | |||
/***/ public String configProxyJumpWithPath; | |||
/***/ public String ftpCloseFailed; |
@@ -32,10 +32,9 @@ import org.apache.sshd.client.ClientBuilder; | |||
import org.apache.sshd.client.SshClient; | |||
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.NamedFactory; | |||
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; | |||
@@ -49,6 +48,7 @@ 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; | |||
import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory; | |||
import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier; | |||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; | |||
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig; | |||
@@ -577,7 +577,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { | |||
// Password auth doesn't have this problem. | |||
return Collections.unmodifiableList( | |||
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE, | |||
UserAuthPublicKeyFactory.INSTANCE, | |||
JGitPublicKeyAuthFactory.FACTORY, | |||
JGitPasswordAuthFactory.INSTANCE, | |||
UserAuthKeyboardInteractiveFactory.INSTANCE)); | |||
} |
@@ -467,4 +467,34 @@ public class OpenSshConfigTest extends RepositoryTestCase { | |||
new File(new File(home, ".ssh"), localhost + "_id_dsa"), | |||
h.getIdentityFile()); | |||
} | |||
@Test | |||
public void testPubKeyAcceptedAlgorithms() throws Exception { | |||
config("Host=orcz\n\tPubkeyAcceptedAlgorithms ^ssh-rsa"); | |||
Host h = osc.lookup("orcz"); | |||
Config c = h.getConfig(); | |||
assertEquals("^ssh-rsa", | |||
c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS)); | |||
assertEquals("^ssh-rsa", c.getValue("PubkeyAcceptedKeyTypes")); | |||
} | |||
@Test | |||
public void testPubKeyAcceptedKeyTypes() throws Exception { | |||
config("Host=orcz\n\tPubkeyAcceptedKeyTypes ^ssh-rsa"); | |||
Host h = osc.lookup("orcz"); | |||
Config c = h.getConfig(); | |||
assertEquals("^ssh-rsa", | |||
c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS)); | |||
assertEquals("^ssh-rsa", c.getValue("PubkeyAcceptedKeyTypes")); | |||
} | |||
@Test | |||
public void testEolComments() throws Exception { | |||
config("#Comment\nHost=orcz #Comment\n\tPubkeyAcceptedAlgorithms ^ssh-rsa # Comment\n#Comment"); | |||
Host h = osc.lookup("orcz"); | |||
assertNotNull(h); | |||
Config c = h.getConfig(); | |||
assertEquals("^ssh-rsa", | |||
c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS)); | |||
} | |||
} |
@@ -530,6 +530,7 @@ peeledRefIsRequired=Peeled ref is required. | |||
peerDidNotSupplyACompleteObjectGraph=peer did not supply a complete object graph | |||
personIdentEmailNonNull=E-mail address of PersonIdent must not be null. | |||
personIdentNameNonNull=Name of PersonIdent must not be null. | |||
postCommitHookFailed=Execution of post-commit hook failed: {0}. | |||
prefixRemote=remote: | |||
problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0} | |||
progressMonUploading=Uploading {0} |
@@ -20,6 +20,7 @@ import java.util.LinkedList; | |||
import java.util.List; | |||
import org.eclipse.jgit.api.errors.AbortedByHookException; | |||
import org.eclipse.jgit.api.errors.CanceledException; | |||
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; | |||
import org.eclipse.jgit.api.errors.EmptyCommitException; | |||
import org.eclipse.jgit.api.errors.GitAPIException; | |||
@@ -36,6 +37,8 @@ import org.eclipse.jgit.dircache.DirCacheBuildIterator; | |||
import org.eclipse.jgit.dircache.DirCacheBuilder; | |||
import org.eclipse.jgit.dircache.DirCacheEntry; | |||
import org.eclipse.jgit.dircache.DirCacheIterator; | |||
import org.eclipse.jgit.errors.IncorrectObjectTypeException; | |||
import org.eclipse.jgit.errors.MissingObjectException; | |||
import org.eclipse.jgit.errors.UnmergedPathException; | |||
import org.eclipse.jgit.hooks.CommitMsgHook; | |||
import org.eclipse.jgit.hooks.Hooks; | |||
@@ -67,6 +70,8 @@ import org.eclipse.jgit.treewalk.FileTreeIterator; | |||
import org.eclipse.jgit.treewalk.TreeWalk; | |||
import org.eclipse.jgit.treewalk.TreeWalk.OperationType; | |||
import org.eclipse.jgit.util.ChangeIdUtil; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
/** | |||
* A class used to execute a {@code Commit} command. It has setters for all | |||
@@ -78,6 +83,9 @@ import org.eclipse.jgit.util.ChangeIdUtil; | |||
* >Git documentation about Commit</a> | |||
*/ | |||
public class CommitCommand extends GitCommand<RevCommit> { | |||
private static final Logger log = LoggerFactory | |||
.getLogger(CommitCommand.class); | |||
private PersonIdent author; | |||
private PersonIdent committer; | |||
@@ -173,8 +181,7 @@ public class CommitCommand extends GitCommand<RevCommit> { | |||
if (all && !repo.isBare()) { | |||
try (Git git = new Git(repo)) { | |||
git.add() | |||
.addFilepattern(".") //$NON-NLS-1$ | |||
git.add().addFilepattern(".") //$NON-NLS-1$ | |||
.setUpdate(true).call(); | |||
} catch (NoFilepatternException e) { | |||
// should really not happen | |||
@@ -212,7 +219,7 @@ public class CommitCommand extends GitCommand<RevCommit> { | |||
.setCommitMessage(message).call(); | |||
} | |||
// lock the index | |||
RevCommit revCommit; | |||
DirCache index = repo.lockDirCache(); | |||
try (ObjectInserter odi = repo.newObjectInserter()) { | |||
if (!only.isEmpty()) | |||
@@ -226,100 +233,37 @@ public class CommitCommand extends GitCommand<RevCommit> { | |||
if (insertChangeId) | |||
insertChangeId(indexTreeId); | |||
// Check for empty commits | |||
if (headId != null && !allowEmpty.booleanValue()) { | |||
RevCommit headCommit = rw.parseCommit(headId); | |||
headCommit.getTree(); | |||
if (indexTreeId.equals(headCommit.getTree())) { | |||
throw new EmptyCommitException( | |||
JGitText.get().emptyCommit); | |||
} | |||
} | |||
checkIfEmpty(rw, headId, indexTreeId); | |||
// Create a Commit object, populate it and write it | |||
CommitBuilder commit = new CommitBuilder(); | |||
commit.setCommitter(committer); | |||
commit.setAuthor(author); | |||
commit.setMessage(message); | |||
commit.setParentIds(parents); | |||
commit.setTreeId(indexTreeId); | |||
if (signCommit.booleanValue()) { | |||
if (gpgSigner == null) { | |||
throw new ServiceUnavailableException( | |||
JGitText.get().signingServiceUnavailable); | |||
} | |||
if (gpgSigner instanceof GpgObjectSigner) { | |||
((GpgObjectSigner) gpgSigner).signObject(commit, | |||
signingKey, committer, credentialsProvider, | |||
gpgConfig); | |||
} else { | |||
if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) { | |||
throw new UnsupportedSigningFormatException(JGitText | |||
.get().onlyOpenPgpSupportedForSigning); | |||
} | |||
gpgSigner.sign(commit, signingKey, committer, | |||
credentialsProvider); | |||
} | |||
sign(commit); | |||
} | |||
ObjectId commitId = odi.insert(commit); | |||
odi.flush(); | |||
revCommit = rw.parseCommit(commitId); | |||
RevCommit revCommit = rw.parseCommit(commitId); | |||
RefUpdate ru = repo.updateRef(Constants.HEAD); | |||
ru.setNewObjectId(commitId); | |||
if (!useDefaultReflogMessage) { | |||
ru.setRefLogMessage(reflogComment, false); | |||
} else { | |||
String prefix = amend ? "commit (amend): " //$NON-NLS-1$ | |||
: parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$ | |||
: "commit: "; //$NON-NLS-1$ | |||
ru.setRefLogMessage(prefix + revCommit.getShortMessage(), | |||
false); | |||
} | |||
if (headId != null) | |||
ru.setExpectedOldObjectId(headId); | |||
else | |||
ru.setExpectedOldObjectId(ObjectId.zeroId()); | |||
Result rc = ru.forceUpdate(); | |||
switch (rc) { | |||
case NEW: | |||
case FORCED: | |||
case FAST_FORWARD: { | |||
setCallable(false); | |||
if (state == RepositoryState.MERGING_RESOLVED | |||
|| isMergeDuringRebase(state)) { | |||
// Commit was successful. Now delete the files | |||
// used for merge commits | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeMergeHeads(null); | |||
} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) { | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeCherryPickHead(null); | |||
} else if (state == RepositoryState.REVERTING_RESOLVED) { | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeRevertHead(null); | |||
} | |||
Hooks.postCommit(repo, | |||
hookOutRedirect.get(PostCommitHook.NAME), | |||
hookErrRedirect.get(PostCommitHook.NAME)).call(); | |||
return revCommit; | |||
} | |||
case REJECTED: | |||
case LOCK_FAILURE: | |||
throw new ConcurrentRefUpdateException( | |||
JGitText.get().couldNotLockHEAD, ru.getRef(), rc); | |||
default: | |||
throw new JGitInternalException(MessageFormat.format( | |||
JGitText.get().updatingRefFailed, Constants.HEAD, | |||
commitId.toString(), rc)); | |||
} | |||
updateRef(state, headId, revCommit, commitId); | |||
} finally { | |||
index.unlock(); | |||
} | |||
try { | |||
Hooks.postCommit(repo, hookOutRedirect.get(PostCommitHook.NAME), | |||
hookErrRedirect.get(PostCommitHook.NAME)).call(); | |||
} catch (Exception e) { | |||
log.error(MessageFormat.format( | |||
JGitText.get().postCommitHookFailed, e.getMessage()), | |||
e); | |||
} | |||
return revCommit; | |||
} catch (UnmergedPathException e) { | |||
throw new UnmergedPathsException(e); | |||
} catch (IOException e) { | |||
@@ -328,6 +272,89 @@ public class CommitCommand extends GitCommand<RevCommit> { | |||
} | |||
} | |||
private void checkIfEmpty(RevWalk rw, ObjectId headId, ObjectId indexTreeId) | |||
throws EmptyCommitException, MissingObjectException, | |||
IncorrectObjectTypeException, IOException { | |||
if (headId != null && !allowEmpty.booleanValue()) { | |||
RevCommit headCommit = rw.parseCommit(headId); | |||
headCommit.getTree(); | |||
if (indexTreeId.equals(headCommit.getTree())) { | |||
throw new EmptyCommitException(JGitText.get().emptyCommit); | |||
} | |||
} | |||
} | |||
private void sign(CommitBuilder commit) throws ServiceUnavailableException, | |||
CanceledException, UnsupportedSigningFormatException { | |||
if (gpgSigner == null) { | |||
throw new ServiceUnavailableException( | |||
JGitText.get().signingServiceUnavailable); | |||
} | |||
if (gpgSigner instanceof GpgObjectSigner) { | |||
((GpgObjectSigner) gpgSigner).signObject(commit, | |||
signingKey, committer, credentialsProvider, | |||
gpgConfig); | |||
} else { | |||
if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) { | |||
throw new UnsupportedSigningFormatException(JGitText | |||
.get().onlyOpenPgpSupportedForSigning); | |||
} | |||
gpgSigner.sign(commit, signingKey, committer, | |||
credentialsProvider); | |||
} | |||
} | |||
private void updateRef(RepositoryState state, ObjectId headId, | |||
RevCommit revCommit, ObjectId commitId) | |||
throws ConcurrentRefUpdateException, IOException { | |||
RefUpdate ru = repo.updateRef(Constants.HEAD); | |||
ru.setNewObjectId(commitId); | |||
if (!useDefaultReflogMessage) { | |||
ru.setRefLogMessage(reflogComment, false); | |||
} else { | |||
String prefix = amend ? "commit (amend): " //$NON-NLS-1$ | |||
: parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$ | |||
: "commit: "; //$NON-NLS-1$ | |||
ru.setRefLogMessage(prefix + revCommit.getShortMessage(), | |||
false); | |||
} | |||
if (headId != null) { | |||
ru.setExpectedOldObjectId(headId); | |||
} else { | |||
ru.setExpectedOldObjectId(ObjectId.zeroId()); | |||
} | |||
Result rc = ru.forceUpdate(); | |||
switch (rc) { | |||
case NEW: | |||
case FORCED: | |||
case FAST_FORWARD: { | |||
setCallable(false); | |||
if (state == RepositoryState.MERGING_RESOLVED | |||
|| isMergeDuringRebase(state)) { | |||
// Commit was successful. Now delete the files | |||
// used for merge commits | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeMergeHeads(null); | |||
} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) { | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeCherryPickHead(null); | |||
} else if (state == RepositoryState.REVERTING_RESOLVED) { | |||
repo.writeMergeCommitMsg(null); | |||
repo.writeRevertHead(null); | |||
} | |||
break; | |||
} | |||
case REJECTED: | |||
case LOCK_FAILURE: | |||
throw new ConcurrentRefUpdateException( | |||
JGitText.get().couldNotLockHEAD, ru.getRef(), rc); | |||
default: | |||
throw new JGitInternalException(MessageFormat.format( | |||
JGitText.get().updatingRefFailed, Constants.HEAD, | |||
commitId.toString(), rc)); | |||
} | |||
} | |||
private void insertChangeId(ObjectId treeId) { | |||
ObjectId firstParentId = null; | |||
if (!parents.isEmpty()) |
@@ -558,6 +558,7 @@ public class JGitText extends TranslationBundle { | |||
/***/ public String peerDidNotSupplyACompleteObjectGraph; | |||
/***/ public String personIdentEmailNonNull; | |||
/***/ public String personIdentNameNonNull; | |||
/***/ public String postCommitHookFailed; | |||
/***/ public String prefixRemote; | |||
/***/ public String problemWithResolvingPushRefSpecsLocally; | |||
/***/ public String progressMonUploading; |
@@ -23,7 +23,6 @@ import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.TreeMap; | |||
@@ -224,8 +223,17 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
entries.put(DEFAULT_NAME, defaults); | |||
while ((line = reader.readLine()) != null) { | |||
// OpenSsh ignores trailing comments on a line. Anything after the | |||
// first # on a line is trimmed away (yes, even if the hash is | |||
// inside quotes). | |||
// | |||
// See https://github.com/openssh/openssh-portable/commit/2bcbf679 | |||
int i = line.indexOf('#'); | |||
if (i >= 0) { | |||
line = line.substring(0, i); | |||
} | |||
line = line.trim(); | |||
if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$ | |||
if (line.isEmpty()) { | |||
continue; | |||
} | |||
String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$ | |||
@@ -484,12 +492,30 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE); | |||
} | |||
/** | |||
* OpenSSH has renamed some config keys. This maps old names to new | |||
* names. | |||
*/ | |||
private static final Map<String, String> ALIASES = new TreeMap<>( | |||
String.CASE_INSENSITIVE_ORDER); | |||
static { | |||
// See https://github.com/openssh/openssh-portable/commit/ee9c0da80 | |||
ALIASES.put("PubkeyAcceptedKeyTypes", //$NON-NLS-1$ | |||
SshConstants.PUBKEY_ACCEPTED_ALGORITHMS); | |||
} | |||
private Map<String, String> options; | |||
private Map<String, List<String>> multiOptions; | |||
private Map<String, List<String>> listOptions; | |||
private static String toKey(String key) { | |||
String k = ALIASES.get(key); | |||
return k != null ? k : key; | |||
} | |||
/** | |||
* Retrieves the value of a single-valued key, or the first if the key | |||
* has multiple values. Keys are case-insensitive, so | |||
@@ -501,15 +527,15 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
*/ | |||
@Override | |||
public String getValue(String key) { | |||
String result = options != null ? options.get(key) : null; | |||
String k = toKey(key); | |||
String result = options != null ? options.get(k) : null; | |||
if (result == null) { | |||
// Let's be lenient and return at least the first value from | |||
// a list-valued or multi-valued key. | |||
List<String> values = listOptions != null ? listOptions.get(key) | |||
List<String> values = listOptions != null ? listOptions.get(k) | |||
: null; | |||
if (values == null) { | |||
values = multiOptions != null ? multiOptions.get(key) | |||
: null; | |||
values = multiOptions != null ? multiOptions.get(k) : null; | |||
} | |||
if (values != null && !values.isEmpty()) { | |||
result = values.get(0); | |||
@@ -529,10 +555,11 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
*/ | |||
@Override | |||
public List<String> getValues(String key) { | |||
List<String> values = listOptions != null ? listOptions.get(key) | |||
String k = toKey(key); | |||
List<String> values = listOptions != null ? listOptions.get(k) | |||
: null; | |||
if (values == null) { | |||
values = multiOptions != null ? multiOptions.get(key) : null; | |||
values = multiOptions != null ? multiOptions.get(k) : null; | |||
} | |||
if (values == null || values.isEmpty()) { | |||
return new ArrayList<>(); | |||
@@ -551,34 +578,35 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
* to set or add | |||
*/ | |||
public void setValue(String key, String value) { | |||
String k = toKey(key); | |||
if (value == null) { | |||
if (multiOptions != null) { | |||
multiOptions.remove(key); | |||
multiOptions.remove(k); | |||
} | |||
if (listOptions != null) { | |||
listOptions.remove(key); | |||
listOptions.remove(k); | |||
} | |||
if (options != null) { | |||
options.remove(key); | |||
options.remove(k); | |||
} | |||
return; | |||
} | |||
if (MULTI_KEYS.contains(key)) { | |||
if (MULTI_KEYS.contains(k)) { | |||
if (multiOptions == null) { | |||
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | |||
} | |||
List<String> values = multiOptions.get(key); | |||
List<String> values = multiOptions.get(k); | |||
if (values == null) { | |||
values = new ArrayList<>(4); | |||
multiOptions.put(key, values); | |||
multiOptions.put(k, values); | |||
} | |||
values.add(value); | |||
} else { | |||
if (options == null) { | |||
options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | |||
} | |||
if (!options.containsKey(key)) { | |||
options.put(key, value); | |||
if (!options.containsKey(k)) { | |||
options.put(k, value); | |||
} | |||
} | |||
} | |||
@@ -595,20 +623,21 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
if (values.isEmpty()) { | |||
return; | |||
} | |||
String k = toKey(key); | |||
// Check multi-valued keys first; because of the replacement | |||
// strategy, they must take precedence over list-valued keys | |||
// which always follow the "first occurrence wins" strategy. | |||
// | |||
// Note that SendEnv is a multi-valued list-valued key. (It's | |||
// rather immaterial for JGit, though.) | |||
if (MULTI_KEYS.contains(key)) { | |||
if (MULTI_KEYS.contains(k)) { | |||
if (multiOptions == null) { | |||
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | |||
} | |||
List<String> items = multiOptions.get(key); | |||
List<String> items = multiOptions.get(k); | |||
if (items == null) { | |||
items = new ArrayList<>(values); | |||
multiOptions.put(key, items); | |||
multiOptions.put(k, items); | |||
} else { | |||
items.addAll(values); | |||
} | |||
@@ -616,8 +645,8 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
if (listOptions == null) { | |||
listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | |||
} | |||
if (!listOptions.containsKey(key)) { | |||
listOptions.put(key, values); | |||
if (!listOptions.containsKey(k)) { | |||
listOptions.put(k, values); | |||
} | |||
} | |||
} | |||
@@ -630,7 +659,7 @@ public class OpenSshConfigFile implements SshConfigStore { | |||
* @return {@code true} if the key is a list-valued key. | |||
*/ | |||
public static boolean isListKey(String key) { | |||
return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT)); | |||
return LIST_KEYS.contains(toKey(key)); | |||
} | |||
void merge(HostEntry entry) { |
@@ -114,6 +114,14 @@ public final class SshConstants { | |||
/** Key in an ssh config file. */ | |||
public static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications"; | |||
/** | |||
* Key in an ssh config file; defines signature algorithms for public key | |||
* authentication as a comma-separated list. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public static final String PUBKEY_ACCEPTED_ALGORITHMS = "PubkeyAcceptedAlgorithms"; | |||
/** Key in an ssh config file. */ | |||
public static final String PROXY_COMMAND = "ProxyCommand"; | |||