From ffc1f9b02618a59ee72298e9af15f64fe157fa8a Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Thu, 18 Mar 2021 21:16:48 +0100 Subject: [PATCH 1/7] sshd: implement ssh config PubkeyAcceptedAlgorithms Apache MINA sshd 2.6.0 appears to use only the first appropriate public key signature algorithm for a particular key. See [1]. For RSA keys, that is rsa-sha2-512. This breaks authentication at servers that only know the older (and deprecated) ssh-rsa algorithm. With PubkeyAcceptedAlgorithms, users can re-order algorithms in the ssh config file per host, if needed. Setting PubkeyAcceptedAlgorithms ^ssh-rsa will put "ssh-rsa" at the front of the list of algorithms, and then authentication at such servers with RSA keys works again. [1] https://issues.apache.org/jira/browse/SSHD-1105 Bug: 572056 Change-Id: I86c3b93f05960c68936e80642965815926bb2532 Signed-off-by: Thomas Wolf --- .../META-INF/MANIFEST.MF | 1 + .../build.properties | 2 + .../jgit/transport/sshd/ApacheSshTest.java | 122 +++++++++++++----- .../transport/sshd/SshdText.properties | 3 +- .../transport/sshd/JGitClientSession.java | 107 +++++++++------ .../transport/sshd/JGitSshClient.java | 18 +++ .../internal/transport/sshd/SshdText.java | 3 +- .../eclipse/jgit/transport/SshConstants.java | 8 ++ 8 files changed, 191 insertions(+), 73 deletions(-) diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index 30eb2bf8b..b03545352 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -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)", diff --git a/org.eclipse.jgit.ssh.apache.test/build.properties b/org.eclipse.jgit.ssh.apache.test/build.properties index 9ffa0caf7..406c5a768 100644 --- a/org.eclipse.jgit.ssh.apache.test/build.properties +++ b/org.eclipse.jgit.ssh.apache.test/build.properties @@ -3,3 +3,5 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ plugin.properties +additional.bundles = org.apache.log4j,\ + org.slf4j.binding.log4j12 diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java index 97f97f902..09d048b4f 100644 --- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java @@ -47,7 +47,9 @@ 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; @@ -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 * 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, SshdSocketAddress[] report) throws Exception { - SshServer proxy = SshServer.setUpDefaultServer(); - // 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)); + SshServer proxy = createServer(user, userKey); // Allow forwarding proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) { @@ -275,21 +316,7 @@ protected boolean checkAcceptance(String request, Session session, } }); proxy.start(); - // 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()); - writer.append(','); - KnownHostHashValue.appendHostPattern(writer, "127.0.0.1", - proxy.getPort()); - writer.append(' '); - PublicKeyEntry.appendPublicKeyEntry(writer, - proxyHostKey.getPublic()); - writer.append('\n'); - } + registerServer(proxy); return proxy; } @@ -606,4 +633,35 @@ 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 bug + * 572056 + * @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(); + } + } } diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index f810fd40e..16b573833 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -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 diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java index 66713ba63..8183a92b9 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java @@ -21,6 +21,7 @@ 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.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 void messageReceived(Readable buffer) throws Exception { @Override protected String resolveAvailableSignaturesProposal( FactoryManager manager) { - Set defaultSignatures = new LinkedHashSet<>(); - defaultSignatures.addAll(getSignatureFactoriesNames()); + List 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)); + if (!StringUtils.isEmptyOrNull(algorithms)) { + List result = modifyAlgorithmList(defaultSignatures, + algorithms, SshConstants.HOST_KEY_ALGORITHMS); + if (!result.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + result); } - return String.join(",", defaultSignatures); //$NON-NLS-1$ - default: - // Default is overridden -- only accept the ones for which we do - // have an implementation. - List 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; + 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 @@ protected String resolveAvailableSignaturesProposal( } } 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 modifyAlgorithmList(List defaultList, + String fromConfig, String overrideKey) { + Set 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 allSignatures = filteredList(defaults, + fromConfig.substring(1)); + Set 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 current, String key, String patterns) { for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$ diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index 74455dc80..071e1979d 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -267,6 +267,24 @@ private JGitClientSession createSession(IoSession ioSession, 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 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()); } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 13bb3ebe7..4c4ff5949 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -25,8 +25,7 @@ public static SshdText get() { /***/ 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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java index fff2938e5..be55cd1b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -114,6 +114,14 @@ private 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"; From 6faee128f8930b851d33f1f06cb77b3e1b9a0cc5 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 19 Mar 2021 09:24:31 +0100 Subject: [PATCH 2/7] sshd: modernize ssh config file parsing OpenSSH has changed some things in ssh config files. Update our parser to implement some of these changes: * ignore trailing comments on a line * rename PubkeyAcceptedKeyTypes to PubkeyAcceptedAlgorithms Note that for the rename, openSSH still accepts both names. We do the same, translating names whenever we get or set values. Change-Id: Icccca060e6a4350a7acf05ff9e260f2c8c60ee1a Signed-off-by: Thomas Wolf --- .../jgit/transport/OpenSshConfigTest.java | 30 ++++++++ .../transport/ssh/OpenSshConfigFile.java | 73 +++++++++++++------ 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index af09f499f..4c7e99ea8 100644 --- a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -467,4 +467,34 @@ public void testLocalhostFQDNReplacement() throws Exception { 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)); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index 98c63cdcd..c514270f5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -23,7 +23,6 @@ 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 @@ private Map parse(BufferedReader reader) 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 static class HostEntry implements SshConfigStore.HostConfig { 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 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 options; private Map> multiOptions; private Map> 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 static class HostEntry implements SshConfigStore.HostConfig { */ @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 values = listOptions != null ? listOptions.get(key) + List 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 String getValue(String key) { */ @Override public List getValues(String key) { - List values = listOptions != null ? listOptions.get(key) + String k = toKey(key); + List 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 List getValues(String key) { * 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 values = multiOptions.get(key); + List 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 void setValue(String key, List values) { 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 items = multiOptions.get(key); + List 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 void setValue(String key, List values) { 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 void setValue(String key, List values) { * @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) { From fd3edc7bfc65f9bdfe785c92c72790261881dd40 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 19 Mar 2021 09:35:34 +0100 Subject: [PATCH 3/7] sshd: try all configured signature algorithms for a key For RSA keys, there may be several configured signature algorithms: rsa-sha2-512, rsa-sha2-256, and ssh-rsa. Upstream sshd has bug SSHD-1105 [1] and always and unconditionally uses only the first configured algorithm. With the default order, this means that it cannot connect to a server that knows only ssh-rsa, like for instance Apache MINA sshd servers older than 2.6.0. This affects for instance bitbucket.org or also AWS Code Commit. Re-introduce our own pubkey authenticator that fixes this. Note that a server may impose a penalty (back-off delay) for subsequent authentication attempts with signature algorithms unknown to the server. In such cases, users can re-order the signature algorithm list via the PubkeyAcceptedAlgorithms (formerly PubkeyAcceptedKeyTypes) ssh config. [1] https://issues.apache.org/jira/browse/SSHD-1105 Bug: 572056 Change-Id: I7fb9c759ab6532e5f3b6524e9084085ddb2f30d6 Signed-off-by: Thomas Wolf --- .../jgit/transport/sshd/ApacheSshTest.java | 38 +++++ .../sshd/JGitPublicKeyAuthFactory.java | 35 +++++ .../sshd/JGitPublicKeyAuthentication.java | 133 ++++++++++++++++++ .../transport/sshd/SshdSessionFactory.java | 6 +- 4 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java index 09d048b4f..c56d2307c 100644 --- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java @@ -664,4 +664,42 @@ public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms() 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. + *

+ * 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. + *

+ * + * @see bug + * 572056 + * @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(); + } + } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java new file mode 100644 index 000000000..0e3e24dcf --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018, 2021 Thomas Wolf 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()); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java new file mode 100644 index 000000000..297b45680 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2018, 2021 Thomas Wolf 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 SSHD-1105 + * @see Bug + * 572056 + */ +public class JGitPublicKeyAuthentication extends UserAuthPublicKey { + + private final List algorithms = new LinkedList<>(); + + JGitPublicKeyAuthentication(List> 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 aliases = new HashSet<>( + KeyUtils.getAllEquivalentKeyTypes(keyType)); + aliases.add(keyType); + List> 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(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index 357994d43..cad959c90 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -32,10 +32,9 @@ 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.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 @@ private List getUserAuthFactories() { // Password auth doesn't have this problem. return Collections.unmodifiableList( Arrays.asList(GssApiWithMicAuthFactory.INSTANCE, - UserAuthPublicKeyFactory.INSTANCE, + JGitPublicKeyAuthFactory.FACTORY, JGitPasswordAuthFactory.INSTANCE, UserAuthKeyboardInteractiveFactory.INSTANCE)); } From f43cb3605c078d0932365c49163f5a8b799fe117 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Sat, 20 Mar 2021 11:15:20 +0100 Subject: [PATCH 4/7] Ensure post-commit hook is called after index lock was released Otherwise a post-commit hook cannot modify the index. Bug: 566934 Change-Id: I0093dccd93b2064f243544b516bdce198afdb18b Signed-off-by: Matthias Sohn --- .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/api/CommitCommand.java | 23 ++++++++++++++----- .../org/eclipse/jgit/internal/JGitText.java | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 9695e5742..d4bcd9a42 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -529,6 +529,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} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 31f6a31c7..51d5d382c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -67,6 +67,8 @@ 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 +80,9 @@ * >Git documentation about Commit */ public class CommitCommand extends GitCommand { + private static final Logger log = LoggerFactory + .getLogger(CommitCommand.class); + private PersonIdent author; private PersonIdent committer; @@ -212,6 +217,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, .setCommitMessage(message).call(); } + RevCommit revCommit; // lock the index DirCache index = repo.lockDirCache(); try (ObjectInserter odi = repo.newObjectInserter()) { @@ -267,7 +273,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, ObjectId commitId = odi.insert(commit); odi.flush(); - RevCommit revCommit = rw.parseCommit(commitId); + revCommit = rw.parseCommit(commitId); RefUpdate ru = repo.updateRef(Constants.HEAD); ru.setNewObjectId(commitId); if (!useDefaultReflogMessage) { @@ -302,11 +308,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, repo.writeMergeCommitMsg(null); repo.writeRevertHead(null); } - Hooks.postCommit(repo, - hookOutRedirect.get(PostCommitHook.NAME), - hookErrRedirect.get(PostCommitHook.NAME)).call(); - - return revCommit; + break; } case REJECTED: case LOCK_FAILURE: @@ -320,6 +322,15 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, } 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) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 95265feb4..fe8c2d218 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -557,6 +557,7 @@ public static JGitText get() { /***/ public String peerDidNotSupplyACompleteObjectGraph; /***/ public String personIdentEmailNonNull; /***/ public String personIdentNameNonNull; + /***/ public String postCommitHookFailed; /***/ public String prefixRemote; /***/ public String problemWithResolvingPushRefSpecsLocally; /***/ public String progressMonUploading; From b08c599fb8f7eae831e3b0fd1f9cfd907db4b098 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Sat, 20 Mar 2021 11:19:07 +0100 Subject: [PATCH 5/7] CommitCommand: remove unncessary comment Let the code speak for itself. Change-Id: I6a6d6c327ffac23fc607295a7f4fd3131b3d1e58 Signed-off-by: Matthias Sohn --- org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java | 1 - 1 file changed, 1 deletion(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 51d5d382c..4291968b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -218,7 +218,6 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, } RevCommit revCommit; - // lock the index DirCache index = repo.lockDirCache(); try (ObjectInserter odi = repo.newObjectInserter()) { if (!only.isEmpty()) From 18c735c474579d013d7e8deb6570b51ec26af087 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Sat, 20 Mar 2021 11:20:52 +0100 Subject: [PATCH 6/7] CommitCommand: fix formatting Change-Id: I5efd1ffee4ebb08b3b5c27e29162493615727840 Signed-off-by: Matthias Sohn --- org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 4291968b4..259a3d269 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -178,8 +178,7 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, 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 From 502bfff7db5c0d91d9c7062fda7a0974df60591a Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Sat, 20 Mar 2021 11:35:27 +0100 Subject: [PATCH 7/7] Refactor CommitCommand to improve readability Change-Id: Id3cac81cd32c07f677b7f669d58e32b5290e1790 Signed-off-by: Matthias Sohn --- .../org/eclipse/jgit/api/CommitCommand.java | 162 ++++++++++-------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 259a3d269..7ec36af71 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -20,6 +20,7 @@ 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.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; @@ -230,93 +233,25 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, 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); - 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)); - } + + updateRef(state, headId, revCommit, commitId); } finally { index.unlock(); } @@ -337,6 +272,89 @@ public RevCommit call() throws GitAPIException, AbortedByHookException, } } + 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())