Merge branch 'stable-5.11'
* 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>
This commit is contained in:
commit
beecca02bb
|
@ -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.helpers;version="[2.6.0,2.7.0)",
|
||||||
org.apache.sshd.common.keyprovider;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.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.net;version="[2.6.0,2.7.0)",
|
||||||
org.apache.sshd.common.util.security;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)",
|
org.apache.sshd.core;version="[2.6.0,2.7.0)",
|
||||||
|
|
|
@ -3,3 +3,5 @@ output.. = bin/
|
||||||
bin.includes = META-INF/,\
|
bin.includes = META-INF/,\
|
||||||
.,\
|
.,\
|
||||||
plugin.properties
|
plugin.properties
|
||||||
|
additional.bundles = org.apache.log4j,\
|
||||||
|
org.slf4j.binding.log4j12
|
||||||
|
|
|
@ -47,7 +47,9 @@
|
||||||
import org.eclipse.jgit.api.errors.TransportException;
|
import org.eclipse.jgit.api.errors.TransportException;
|
||||||
import org.eclipse.jgit.junit.ssh.SshTestBase;
|
import org.eclipse.jgit.junit.ssh.SshTestBase;
|
||||||
import org.eclipse.jgit.lib.Constants;
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.transport.RemoteSession;
|
||||||
import org.eclipse.jgit.transport.SshSessionFactory;
|
import org.eclipse.jgit.transport.SshSessionFactory;
|
||||||
|
import org.eclipse.jgit.transport.URIish;
|
||||||
import org.eclipse.jgit.util.FS;
|
import org.eclipse.jgit.util.FS;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.experimental.theories.Theories;
|
import org.junit.experimental.theories.Theories;
|
||||||
|
@ -231,6 +233,61 @@ public void testCloneAndFetchWithSessionLimit() throws Exception {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a simple SSH server without git setup.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* to accept
|
||||||
|
* @param userKey
|
||||||
|
* public key of that user at this server
|
||||||
|
* @return the {@link SshServer}, not yet started
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
srv.setKeyPairProvider(
|
||||||
|
session -> Collections.singletonList(proxyHostKey));
|
||||||
|
// Allow (only) publickey authentication
|
||||||
|
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);
|
||||||
|
srv.setPublickeyAuthenticator(
|
||||||
|
(userName, publicKey, session) -> user.equals(userName)
|
||||||
|
&& KeyUtils.compareKeys(userProxyKey, publicKey));
|
||||||
|
return srv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
srv.getPort());
|
||||||
|
writer.append(',');
|
||||||
|
KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
|
||||||
|
srv.getPort());
|
||||||
|
writer.append(' ');
|
||||||
|
PublicKeyEntry.appendPublicKeyEntry(writer,
|
||||||
|
srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic());
|
||||||
|
writer.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a simple proxy server. Accepts only publickey authentication from
|
* Creates a simple proxy server. Accepts only publickey authentication from
|
||||||
* the given user with the given key, allows all forwardings. Adds the
|
* the given user with the given key, allows all forwardings. Adds the
|
||||||
|
@ -247,23 +304,7 @@ public void testCloneAndFetchWithSessionLimit() throws Exception {
|
||||||
*/
|
*/
|
||||||
private SshServer createProxy(String user, File userKey,
|
private SshServer createProxy(String user, File userKey,
|
||||||
SshdSocketAddress[] report) throws Exception {
|
SshdSocketAddress[] report) throws Exception {
|
||||||
SshServer proxy = SshServer.setUpDefaultServer();
|
SshServer proxy = createServer(user, userKey);
|
||||||
// Give the server its own host key
|
|
||||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
|
||||||
generator.initialize(2048);
|
|
||||||
KeyPair proxyHostKey = generator.generateKeyPair();
|
|
||||||
proxy.setKeyPairProvider(
|
|
||||||
session -> Collections.singletonList(proxyHostKey));
|
|
||||||
// Allow (only) publickey authentication
|
|
||||||
proxy.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(
|
|
||||||
(userName, publicKey, session) -> user.equals(userName)
|
|
||||||
&& KeyUtils.compareKeys(userProxyKey, publicKey));
|
|
||||||
// Allow forwarding
|
// Allow forwarding
|
||||||
proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) {
|
proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) {
|
||||||
|
|
||||||
|
@ -275,21 +316,7 @@ protected boolean checkAcceptance(String request, Session session,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
proxy.start();
|
proxy.start();
|
||||||
// Add the proxy's host key to knownhosts
|
registerServer(proxy);
|
||||||
try (BufferedWriter writer = Files.newBufferedWriter(
|
|
||||||
knownHosts.toPath(), StandardCharsets.US_ASCII,
|
|
||||||
StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
|
|
||||||
writer.append('\n');
|
|
||||||
KnownHostHashValue.appendHostPattern(writer, "localhost",
|
|
||||||
proxy.getPort());
|
|
||||||
writer.append(',');
|
|
||||||
KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
|
|
||||||
proxy.getPort());
|
|
||||||
writer.append(' ');
|
|
||||||
PublicKeyEntry.appendPublicKeyEntry(writer,
|
|
||||||
proxyHostKey.getPublic());
|
|
||||||
writer.append('\n');
|
|
||||||
}
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -606,4 +633,73 @@ public void testJumpHostRecursion() throws Exception {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}
|
configInvalidPattern=Invalid pattern in ssh config key {0}: {1}
|
||||||
configInvalidPositive=Ssh config entry {0} must be a strictly positive number but is ''{1}''
|
configInvalidPositive=Ssh config entry {0} must be a strictly positive number but is ''{1}''
|
||||||
configInvalidProxyJump=Ssh config, host ''{0}'': Cannot parse ProxyJump ''{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.
|
configNoKnownAlgorithms=Ssh config ''{0}'' ''{1}'' resulted in empty list (none known, or all known removed); using default.
|
||||||
configNoRemainingHostKeyAlgorithms=Ssh config removed all host key algorithms: HostKeyAlgorithms ''{0}''
|
|
||||||
configProxyJumpNotSsh=Non-ssh URI in ProxyJump ssh config
|
configProxyJumpNotSsh=Non-ssh URI in ProxyJump ssh config
|
||||||
configProxyJumpWithPath=ProxyJump ssh config: jump host specification must not have a path
|
configProxyJumpWithPath=ProxyJump ssh config: jump host specification must not have a path
|
||||||
ftpCloseFailed=Closing the SFTP channel failed
|
ftpCloseFailed=Closing the SFTP channel failed
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
|
import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||||
import org.eclipse.jgit.transport.SshConstants;
|
import org.eclipse.jgit.transport.SshConstants;
|
||||||
|
import org.eclipse.jgit.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can
|
* A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can
|
||||||
|
@ -201,48 +203,23 @@ public void messageReceived(Readable buffer) throws Exception {
|
||||||
@Override
|
@Override
|
||||||
protected String resolveAvailableSignaturesProposal(
|
protected String resolveAvailableSignaturesProposal(
|
||||||
FactoryManager manager) {
|
FactoryManager manager) {
|
||||||
Set<String> defaultSignatures = new LinkedHashSet<>();
|
List<String> defaultSignatures = getSignatureFactoriesNames();
|
||||||
defaultSignatures.addAll(getSignatureFactoriesNames());
|
|
||||||
HostConfigEntry config = resolveAttribute(
|
HostConfigEntry config = resolveAttribute(
|
||||||
JGitSshClient.HOST_CONFIG_ENTRY);
|
JGitSshClient.HOST_CONFIG_ENTRY);
|
||||||
String hostKeyAlgorithms = config
|
String algorithms = config
|
||||||
.getProperty(SshConstants.HOST_KEY_ALGORITHMS);
|
.getProperty(SshConstants.HOST_KEY_ALGORITHMS);
|
||||||
if (hostKeyAlgorithms != null && !hostKeyAlgorithms.isEmpty()) {
|
if (!StringUtils.isEmptyOrNull(algorithms)) {
|
||||||
char first = hostKeyAlgorithms.charAt(0);
|
List<String> result = modifyAlgorithmList(defaultSignatures,
|
||||||
switch (first) {
|
algorithms, SshConstants.HOST_KEY_ALGORITHMS);
|
||||||
case '+':
|
if (!result.isEmpty()) {
|
||||||
// Additions make not much sense -- it's either in
|
if (log.isDebugEnabled()) {
|
||||||
// defaultSignatures already, or we have no implementation for
|
log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + result);
|
||||||
// it. No point in proposing it.
|
}
|
||||||
return String.join(",", defaultSignatures); //$NON-NLS-1$
|
return String.join(",", result); //$NON-NLS-1$
|
||||||
case '-':
|
}
|
||||||
// This takes wildcard patterns!
|
log.warn(format(SshdText.get().configNoKnownAlgorithms,
|
||||||
removeFromList(defaultSignatures,
|
|
||||||
SshConstants.HOST_KEY_ALGORITHMS,
|
SshConstants.HOST_KEY_ALGORITHMS,
|
||||||
hostKeyAlgorithms.substring(1));
|
algorithms));
|
||||||
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$
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// No HostKeyAlgorithms; using default -- change order to put existing
|
// No HostKeyAlgorithms; using default -- change order to put existing
|
||||||
// keys first.
|
// keys first.
|
||||||
|
@ -262,11 +239,67 @@ protected String resolveAvailableSignaturesProposal(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reordered.addAll(defaultSignatures);
|
reordered.addAll(defaultSignatures);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + reordered);
|
||||||
|
}
|
||||||
return String.join(",", reordered); //$NON-NLS-1$
|
return String.join(",", reordered); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(
|
||||||
|
SshConstants.HOST_KEY_ALGORITHMS + ' ' + defaultSignatures);
|
||||||
|
}
|
||||||
return String.join(",", defaultSignatures); //$NON-NLS-1$
|
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,
|
private void removeFromList(Set<String> current, String key,
|
||||||
String patterns) {
|
String patterns) {
|
||||||
for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$
|
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 @@ private JGitClientSession createSession(IoSession ioSession,
|
||||||
session.setUsername(username);
|
session.setUsername(username);
|
||||||
session.setConnectAddress(address);
|
session.setConnectAddress(address);
|
||||||
session.setHostConfigEntry(hostConfig);
|
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) {
|
if (session.getCredentialsProvider() == null) {
|
||||||
session.setCredentialsProvider(getCredentialsProvider());
|
session.setCredentialsProvider(getCredentialsProvider());
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,7 @@ public static SshdText get() {
|
||||||
/***/ public String configInvalidPattern;
|
/***/ public String configInvalidPattern;
|
||||||
/***/ public String configInvalidPositive;
|
/***/ public String configInvalidPositive;
|
||||||
/***/ public String configInvalidProxyJump;
|
/***/ public String configInvalidProxyJump;
|
||||||
/***/ public String configNoKnownHostKeyAlgorithms;
|
/***/ public String configNoKnownAlgorithms;
|
||||||
/***/ public String configNoRemainingHostKeyAlgorithms;
|
|
||||||
/***/ public String configProxyJumpNotSsh;
|
/***/ public String configProxyJumpNotSsh;
|
||||||
/***/ public String configProxyJumpWithPath;
|
/***/ public String configProxyJumpWithPath;
|
||||||
/***/ public String ftpCloseFailed;
|
/***/ public String ftpCloseFailed;
|
||||||
|
|
|
@ -32,10 +32,9 @@
|
||||||
import org.apache.sshd.client.SshClient;
|
import org.apache.sshd.client.SshClient;
|
||||||
import org.apache.sshd.client.auth.UserAuthFactory;
|
import org.apache.sshd.client.auth.UserAuthFactory;
|
||||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
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.client.config.hosts.HostConfigEntryResolver;
|
||||||
import org.apache.sshd.common.SshException;
|
|
||||||
import org.apache.sshd.common.NamedFactory;
|
import org.apache.sshd.common.NamedFactory;
|
||||||
|
import org.apache.sshd.common.SshException;
|
||||||
import org.apache.sshd.common.compression.BuiltinCompressions;
|
import org.apache.sshd.common.compression.BuiltinCompressions;
|
||||||
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||||
import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
|
import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
|
||||||
|
@ -49,6 +48,7 @@
|
||||||
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
|
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.JGitServerKeyVerifier;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
||||||
|
@ -577,7 +577,7 @@ private List<UserAuthFactory> getUserAuthFactories() {
|
||||||
// Password auth doesn't have this problem.
|
// Password auth doesn't have this problem.
|
||||||
return Collections.unmodifiableList(
|
return Collections.unmodifiableList(
|
||||||
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
|
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
|
||||||
UserAuthPublicKeyFactory.INSTANCE,
|
JGitPublicKeyAuthFactory.FACTORY,
|
||||||
JGitPasswordAuthFactory.INSTANCE,
|
JGitPasswordAuthFactory.INSTANCE,
|
||||||
UserAuthKeyboardInteractiveFactory.INSTANCE));
|
UserAuthKeyboardInteractiveFactory.INSTANCE));
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,4 +467,34 @@ public void testLocalhostFQDNReplacement() throws Exception {
|
||||||
new File(new File(home, ".ssh"), localhost + "_id_dsa"),
|
new File(new File(home, ".ssh"), localhost + "_id_dsa"),
|
||||||
h.getIdentityFile());
|
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
|
peerDidNotSupplyACompleteObjectGraph=peer did not supply a complete object graph
|
||||||
personIdentEmailNonNull=E-mail address of PersonIdent must not be null.
|
personIdentEmailNonNull=E-mail address of PersonIdent must not be null.
|
||||||
personIdentNameNonNull=Name of PersonIdent must not be null.
|
personIdentNameNonNull=Name of PersonIdent must not be null.
|
||||||
|
postCommitHookFailed=Execution of post-commit hook failed: {0}.
|
||||||
prefixRemote=remote:
|
prefixRemote=remote:
|
||||||
problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
|
problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
|
||||||
progressMonUploading=Uploading {0}
|
progressMonUploading=Uploading {0}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.errors.AbortedByHookException;
|
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.ConcurrentRefUpdateException;
|
||||||
import org.eclipse.jgit.api.errors.EmptyCommitException;
|
import org.eclipse.jgit.api.errors.EmptyCommitException;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
@ -36,6 +37,8 @@
|
||||||
import org.eclipse.jgit.dircache.DirCacheBuilder;
|
import org.eclipse.jgit.dircache.DirCacheBuilder;
|
||||||
import org.eclipse.jgit.dircache.DirCacheEntry;
|
import org.eclipse.jgit.dircache.DirCacheEntry;
|
||||||
import org.eclipse.jgit.dircache.DirCacheIterator;
|
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.errors.UnmergedPathException;
|
||||||
import org.eclipse.jgit.hooks.CommitMsgHook;
|
import org.eclipse.jgit.hooks.CommitMsgHook;
|
||||||
import org.eclipse.jgit.hooks.Hooks;
|
import org.eclipse.jgit.hooks.Hooks;
|
||||||
|
@ -67,6 +70,8 @@
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
|
import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
|
||||||
import org.eclipse.jgit.util.ChangeIdUtil;
|
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
|
* A class used to execute a {@code Commit} command. It has setters for all
|
||||||
|
@ -78,6 +83,9 @@
|
||||||
* >Git documentation about Commit</a>
|
* >Git documentation about Commit</a>
|
||||||
*/
|
*/
|
||||||
public class CommitCommand extends GitCommand<RevCommit> {
|
public class CommitCommand extends GitCommand<RevCommit> {
|
||||||
|
private static final Logger log = LoggerFactory
|
||||||
|
.getLogger(CommitCommand.class);
|
||||||
|
|
||||||
private PersonIdent author;
|
private PersonIdent author;
|
||||||
|
|
||||||
private PersonIdent committer;
|
private PersonIdent committer;
|
||||||
|
@ -173,8 +181,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
|
|
||||||
if (all && !repo.isBare()) {
|
if (all && !repo.isBare()) {
|
||||||
try (Git git = new Git(repo)) {
|
try (Git git = new Git(repo)) {
|
||||||
git.add()
|
git.add().addFilepattern(".") //$NON-NLS-1$
|
||||||
.addFilepattern(".") //$NON-NLS-1$
|
|
||||||
.setUpdate(true).call();
|
.setUpdate(true).call();
|
||||||
} catch (NoFilepatternException e) {
|
} catch (NoFilepatternException e) {
|
||||||
// should really not happen
|
// should really not happen
|
||||||
|
@ -212,7 +219,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
.setCommitMessage(message).call();
|
.setCommitMessage(message).call();
|
||||||
}
|
}
|
||||||
|
|
||||||
// lock the index
|
RevCommit revCommit;
|
||||||
DirCache index = repo.lockDirCache();
|
DirCache index = repo.lockDirCache();
|
||||||
try (ObjectInserter odi = repo.newObjectInserter()) {
|
try (ObjectInserter odi = repo.newObjectInserter()) {
|
||||||
if (!only.isEmpty())
|
if (!only.isEmpty())
|
||||||
|
@ -226,26 +233,59 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
if (insertChangeId)
|
if (insertChangeId)
|
||||||
insertChangeId(indexTreeId);
|
insertChangeId(indexTreeId);
|
||||||
|
|
||||||
// Check for empty commits
|
checkIfEmpty(rw, headId, indexTreeId);
|
||||||
if (headId != null && !allowEmpty.booleanValue()) {
|
|
||||||
RevCommit headCommit = rw.parseCommit(headId);
|
|
||||||
headCommit.getTree();
|
|
||||||
if (indexTreeId.equals(headCommit.getTree())) {
|
|
||||||
throw new EmptyCommitException(
|
|
||||||
JGitText.get().emptyCommit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Commit object, populate it and write it
|
// Create a Commit object, populate it and write it
|
||||||
CommitBuilder commit = new CommitBuilder();
|
CommitBuilder commit = new CommitBuilder();
|
||||||
commit.setCommitter(committer);
|
commit.setCommitter(committer);
|
||||||
commit.setAuthor(author);
|
commit.setAuthor(author);
|
||||||
commit.setMessage(message);
|
commit.setMessage(message);
|
||||||
|
|
||||||
commit.setParentIds(parents);
|
commit.setParentIds(parents);
|
||||||
commit.setTreeId(indexTreeId);
|
commit.setTreeId(indexTreeId);
|
||||||
|
|
||||||
if (signCommit.booleanValue()) {
|
if (signCommit.booleanValue()) {
|
||||||
|
sign(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId commitId = odi.insert(commit);
|
||||||
|
odi.flush();
|
||||||
|
revCommit = rw.parseCommit(commitId);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new JGitInternalException(
|
||||||
|
JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (gpgSigner == null) {
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
JGitText.get().signingServiceUnavailable);
|
JGitText.get().signingServiceUnavailable);
|
||||||
|
@ -264,10 +304,9 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectId commitId = odi.insert(commit);
|
private void updateRef(RepositoryState state, ObjectId headId,
|
||||||
odi.flush();
|
RevCommit revCommit, ObjectId commitId)
|
||||||
|
throws ConcurrentRefUpdateException, IOException {
|
||||||
RevCommit revCommit = rw.parseCommit(commitId);
|
|
||||||
RefUpdate ru = repo.updateRef(Constants.HEAD);
|
RefUpdate ru = repo.updateRef(Constants.HEAD);
|
||||||
ru.setNewObjectId(commitId);
|
ru.setNewObjectId(commitId);
|
||||||
if (!useDefaultReflogMessage) {
|
if (!useDefaultReflogMessage) {
|
||||||
|
@ -279,10 +318,11 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
|
ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
if (headId != null)
|
if (headId != null) {
|
||||||
ru.setExpectedOldObjectId(headId);
|
ru.setExpectedOldObjectId(headId);
|
||||||
else
|
} else {
|
||||||
ru.setExpectedOldObjectId(ObjectId.zeroId());
|
ru.setExpectedOldObjectId(ObjectId.zeroId());
|
||||||
|
}
|
||||||
Result rc = ru.forceUpdate();
|
Result rc = ru.forceUpdate();
|
||||||
switch (rc) {
|
switch (rc) {
|
||||||
case NEW:
|
case NEW:
|
||||||
|
@ -302,11 +342,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
repo.writeMergeCommitMsg(null);
|
repo.writeMergeCommitMsg(null);
|
||||||
repo.writeRevertHead(null);
|
repo.writeRevertHead(null);
|
||||||
}
|
}
|
||||||
Hooks.postCommit(repo,
|
break;
|
||||||
hookOutRedirect.get(PostCommitHook.NAME),
|
|
||||||
hookErrRedirect.get(PostCommitHook.NAME)).call();
|
|
||||||
|
|
||||||
return revCommit;
|
|
||||||
}
|
}
|
||||||
case REJECTED:
|
case REJECTED:
|
||||||
case LOCK_FAILURE:
|
case LOCK_FAILURE:
|
||||||
|
@ -317,15 +353,6 @@ public RevCommit call() throws GitAPIException, AbortedByHookException,
|
||||||
JGitText.get().updatingRefFailed, Constants.HEAD,
|
JGitText.get().updatingRefFailed, Constants.HEAD,
|
||||||
commitId.toString(), rc));
|
commitId.toString(), rc));
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
index.unlock();
|
|
||||||
}
|
|
||||||
} catch (UnmergedPathException e) {
|
|
||||||
throw new UnmergedPathsException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new JGitInternalException(
|
|
||||||
JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertChangeId(ObjectId treeId) {
|
private void insertChangeId(ObjectId treeId) {
|
||||||
|
|
|
@ -558,6 +558,7 @@ public static JGitText get() {
|
||||||
/***/ public String peerDidNotSupplyACompleteObjectGraph;
|
/***/ public String peerDidNotSupplyACompleteObjectGraph;
|
||||||
/***/ public String personIdentEmailNonNull;
|
/***/ public String personIdentEmailNonNull;
|
||||||
/***/ public String personIdentNameNonNull;
|
/***/ public String personIdentNameNonNull;
|
||||||
|
/***/ public String postCommitHookFailed;
|
||||||
/***/ public String prefixRemote;
|
/***/ public String prefixRemote;
|
||||||
/***/ public String problemWithResolvingPushRefSpecsLocally;
|
/***/ public String problemWithResolvingPushRefSpecsLocally;
|
||||||
/***/ public String progressMonUploading;
|
/***/ public String progressMonUploading;
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
@ -224,8 +223,17 @@ private Map<String, HostEntry> parse(BufferedReader reader)
|
||||||
entries.put(DEFAULT_NAME, defaults);
|
entries.put(DEFAULT_NAME, defaults);
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
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();
|
line = line.trim();
|
||||||
if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
|
if (line.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
|
String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
|
||||||
|
@ -484,12 +492,30 @@ public static class HostEntry implements SshConfigStore.HostConfig {
|
||||||
LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
|
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, String> options;
|
||||||
|
|
||||||
private Map<String, List<String>> multiOptions;
|
private Map<String, List<String>> multiOptions;
|
||||||
|
|
||||||
private Map<String, List<String>> listOptions;
|
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
|
* Retrieves the value of a single-valued key, or the first if the key
|
||||||
* has multiple values. Keys are case-insensitive, so
|
* has multiple values. Keys are case-insensitive, so
|
||||||
|
@ -501,15 +527,15 @@ public static class HostEntry implements SshConfigStore.HostConfig {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String getValue(String key) {
|
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) {
|
if (result == null) {
|
||||||
// Let's be lenient and return at least the first value from
|
// Let's be lenient and return at least the first value from
|
||||||
// a list-valued or multi-valued key.
|
// a list-valued or multi-valued key.
|
||||||
List<String> values = listOptions != null ? listOptions.get(key)
|
List<String> values = listOptions != null ? listOptions.get(k)
|
||||||
: null;
|
: null;
|
||||||
if (values == null) {
|
if (values == null) {
|
||||||
values = multiOptions != null ? multiOptions.get(key)
|
values = multiOptions != null ? multiOptions.get(k) : null;
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
if (values != null && !values.isEmpty()) {
|
if (values != null && !values.isEmpty()) {
|
||||||
result = values.get(0);
|
result = values.get(0);
|
||||||
|
@ -529,10 +555,11 @@ public String getValue(String key) {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<String> getValues(String key) {
|
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;
|
: null;
|
||||||
if (values == null) {
|
if (values == null) {
|
||||||
values = multiOptions != null ? multiOptions.get(key) : null;
|
values = multiOptions != null ? multiOptions.get(k) : null;
|
||||||
}
|
}
|
||||||
if (values == null || values.isEmpty()) {
|
if (values == null || values.isEmpty()) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
|
@ -551,34 +578,35 @@ public List<String> getValues(String key) {
|
||||||
* to set or add
|
* to set or add
|
||||||
*/
|
*/
|
||||||
public void setValue(String key, String value) {
|
public void setValue(String key, String value) {
|
||||||
|
String k = toKey(key);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (multiOptions != null) {
|
if (multiOptions != null) {
|
||||||
multiOptions.remove(key);
|
multiOptions.remove(k);
|
||||||
}
|
}
|
||||||
if (listOptions != null) {
|
if (listOptions != null) {
|
||||||
listOptions.remove(key);
|
listOptions.remove(k);
|
||||||
}
|
}
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
options.remove(key);
|
options.remove(k);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (MULTI_KEYS.contains(key)) {
|
if (MULTI_KEYS.contains(k)) {
|
||||||
if (multiOptions == null) {
|
if (multiOptions == null) {
|
||||||
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
}
|
}
|
||||||
List<String> values = multiOptions.get(key);
|
List<String> values = multiOptions.get(k);
|
||||||
if (values == null) {
|
if (values == null) {
|
||||||
values = new ArrayList<>(4);
|
values = new ArrayList<>(4);
|
||||||
multiOptions.put(key, values);
|
multiOptions.put(k, values);
|
||||||
}
|
}
|
||||||
values.add(value);
|
values.add(value);
|
||||||
} else {
|
} else {
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
}
|
}
|
||||||
if (!options.containsKey(key)) {
|
if (!options.containsKey(k)) {
|
||||||
options.put(key, value);
|
options.put(k, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -595,20 +623,21 @@ public void setValue(String key, List<String> values) {
|
||||||
if (values.isEmpty()) {
|
if (values.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String k = toKey(key);
|
||||||
// Check multi-valued keys first; because of the replacement
|
// Check multi-valued keys first; because of the replacement
|
||||||
// strategy, they must take precedence over list-valued keys
|
// strategy, they must take precedence over list-valued keys
|
||||||
// which always follow the "first occurrence wins" strategy.
|
// which always follow the "first occurrence wins" strategy.
|
||||||
//
|
//
|
||||||
// Note that SendEnv is a multi-valued list-valued key. (It's
|
// Note that SendEnv is a multi-valued list-valued key. (It's
|
||||||
// rather immaterial for JGit, though.)
|
// rather immaterial for JGit, though.)
|
||||||
if (MULTI_KEYS.contains(key)) {
|
if (MULTI_KEYS.contains(k)) {
|
||||||
if (multiOptions == null) {
|
if (multiOptions == null) {
|
||||||
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
}
|
}
|
||||||
List<String> items = multiOptions.get(key);
|
List<String> items = multiOptions.get(k);
|
||||||
if (items == null) {
|
if (items == null) {
|
||||||
items = new ArrayList<>(values);
|
items = new ArrayList<>(values);
|
||||||
multiOptions.put(key, items);
|
multiOptions.put(k, items);
|
||||||
} else {
|
} else {
|
||||||
items.addAll(values);
|
items.addAll(values);
|
||||||
}
|
}
|
||||||
|
@ -616,8 +645,8 @@ public void setValue(String key, List<String> values) {
|
||||||
if (listOptions == null) {
|
if (listOptions == null) {
|
||||||
listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
}
|
}
|
||||||
if (!listOptions.containsKey(key)) {
|
if (!listOptions.containsKey(k)) {
|
||||||
listOptions.put(key, values);
|
listOptions.put(k, values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -630,7 +659,7 @@ public void setValue(String key, List<String> values) {
|
||||||
* @return {@code true} if the key is a list-valued key.
|
* @return {@code true} if the key is a list-valued key.
|
||||||
*/
|
*/
|
||||||
public static boolean isListKey(String 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) {
|
void merge(HostEntry entry) {
|
||||||
|
|
|
@ -114,6 +114,14 @@ private SshConstants() {
|
||||||
/** Key in an ssh config file. */
|
/** Key in an ssh config file. */
|
||||||
public static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications";
|
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. */
|
/** Key in an ssh config file. */
|
||||||
public static final String PROXY_COMMAND = "ProxyCommand";
|
public static final String PROXY_COMMAND = "ProxyCommand";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue