Merge "Upgrade ecj to 3.25.0"
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index defa710..39af83d 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -59,6 +59,8 @@
org.apache.sshd.common.helpers;version="[2.6.0,2.7.0)",
org.apache.sshd.common.io;version="[2.6.0,2.7.0)",
org.apache.sshd.common.kex;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.kex.extension;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.kex.extension.parser;version="[2.6.0,2.7.0)",
org.apache.sshd.common.keyprovider;version="[2.6.0,2.7.0)",
org.apache.sshd.common.mac;version="[2.6.0,2.7.0)",
org.apache.sshd.common.random;version="[2.6.0,2.7.0)",
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 16b5738..5bc0867 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
@@ -24,7 +24,6 @@
keyEncryptedRetry=Encrypted key ''{0}'' could not be decrypted. Enter the passphrase again.
keyLoadFailed=Could not load key ''{0}''
knownHostsCouldNotUpdate=Could not update known hosts file {0}
-knownHostsFileLockedRead=Could not read known hosts file (locked) {0}
knownHostsFileLockedUpdate=Could not update known hosts file (locked) {0}
knownHostsFileReadFailed=Failed to read known hosts file {0}
knownHostsInvalidLine=Known hosts file {0} contains invalid line {1}
@@ -76,6 +75,9 @@
proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2}
proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0}
proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2}
+pubkeyAuthWrongCommand=Public key authentication received unknown SSH command {0} from {1} ({2})
+pubkeyAuthWrongKey=Public key authentication received wrong key; sent {0}, got back {1} from {2} ({3})
+pubkeyAuthWrongSignatureAlgorithm=Public key authentication requested signature type {0} but got back {1} from {2} ({3})
serverIdNotReceived=No server identification received within {0} bytes
serverIdTooLong=Server identification is longer than 255 characters (including line ending): {0}
serverIdWithNul=Server identification contains a NUL character: {0}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitKexExtensionHandler.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitKexExtensionHandler.java
new file mode 100644
index 0000000..489c77d
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitKexExtensionHandler.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 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.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
+import org.apache.sshd.common.kex.extension.KexExtensions;
+import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * Do not use the DefaultClientKexExtensionHandler from sshd; it doesn't work
+ * properly because of misconceptions. See SSHD-1141.
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/SSHD-1141">SSHD-1141</a>
+ */
+public class JGitKexExtensionHandler extends AbstractLoggingBean
+ implements KexExtensionHandler {
+
+ /** Singleton instance. */
+ public static final JGitKexExtensionHandler INSTANCE = new JGitKexExtensionHandler();
+
+ /**
+ * Session {@link AttributeKey} used to store whether the extension
+ * indicator was already sent.
+ */
+ private static final AttributeKey<Boolean> CLIENT_PROPOSAL_MADE = new AttributeKey<>();
+
+ /**
+ * Session {@link AttributeKey} storing the algorithms announced by the
+ * server as known.
+ */
+ public static final AttributeKey<Set<String>> SERVER_ALGORITHMS = new AttributeKey<>();
+
+ private JGitKexExtensionHandler() {
+ // No public instantiation for singleton
+ }
+
+ @Override
+ public boolean isKexExtensionsAvailable(Session session,
+ AvailabilityPhase phase) throws IOException {
+ return !AvailabilityPhase.PREKEX.equals(phase);
+ }
+
+ @Override
+ public void handleKexInitProposal(Session session, boolean initiator,
+ Map<KexProposalOption, String> proposal) throws IOException {
+ // If it's the very first time, we may add the marker telling the server
+ // that we are ready to handle SSH_MSG_EXT_INFO
+ if (session == null || session.isServerSession() || !initiator) {
+ return;
+ }
+ if (session.getAttribute(CLIENT_PROPOSAL_MADE) != null) {
+ return;
+ }
+ String kexAlgorithms = proposal.get(KexProposalOption.SERVERKEYS);
+ if (StringUtils.isEmptyOrNull(kexAlgorithms)) {
+ return;
+ }
+ List<String> algorithms = new ArrayList<>();
+ // We're a client. We mustn't send the server extension, and we should
+ // send the client extension only once.
+ for (String algo : kexAlgorithms.split(",")) { //$NON-NLS-1$
+ if (KexExtensions.CLIENT_KEX_EXTENSION.equalsIgnoreCase(algo)
+ || KexExtensions.SERVER_KEX_EXTENSION
+ .equalsIgnoreCase(algo)) {
+ continue;
+ }
+ algorithms.add(algo);
+ }
+ // Tell the server that we want to receive SSH2_MSG_EXT_INFO
+ algorithms.add(KexExtensions.CLIENT_KEX_EXTENSION);
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "handleKexInitProposal({}): proposing HostKeyAlgorithms {}", //$NON-NLS-1$
+ session, algorithms);
+ }
+ proposal.put(KexProposalOption.SERVERKEYS,
+ String.join(",", algorithms)); //$NON-NLS-1$
+ session.setAttribute(CLIENT_PROPOSAL_MADE, Boolean.TRUE);
+ }
+
+ @Override
+ public boolean handleKexExtensionRequest(Session session, int index,
+ int count, String name, byte[] data) throws IOException {
+ if (ServerSignatureAlgorithms.NAME.equals(name)) {
+ handleServerSignatureAlgorithms(session,
+ ServerSignatureAlgorithms.INSTANCE.parseExtension(data));
+ }
+ return true;
+ }
+
+ /**
+ * Perform updates after a server-sig-algs extension has been received.
+ *
+ * @param session
+ * the message was received for
+ * @param serverAlgorithms
+ * signature algorithm names announced by the server
+ */
+ protected void handleServerSignatureAlgorithms(Session session,
+ Collection<String> serverAlgorithms) {
+ if (log.isDebugEnabled()) {
+ log.debug("handleServerSignatureAlgorithms({}): {}", session, //$NON-NLS-1$
+ serverAlgorithms);
+ }
+ // Client determines order; server says what it supports. Re-order
+ // such that supported ones are at the front, in client order,
+ // followed by unsupported ones, also in client order.
+ if (serverAlgorithms != null && !serverAlgorithms.isEmpty()) {
+ List<NamedFactory<Signature>> clientAlgorithms = session
+ .getSignatureFactories();
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "handleServerSignatureAlgorithms({}): PubkeyAcceptedAlgorithms before: {}", //$NON-NLS-1$
+ session, clientAlgorithms);
+ }
+ List<NamedFactory<Signature>> unknown = new ArrayList<>();
+ Set<String> known = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ known.addAll(serverAlgorithms);
+ for (Iterator<NamedFactory<Signature>> iter = clientAlgorithms
+ .iterator(); iter.hasNext();) {
+ NamedFactory<Signature> algo = iter.next();
+ if (!known.contains(algo.getName())) {
+ unknown.add(algo);
+ iter.remove();
+ }
+ }
+ // Re-add the unknown ones at the end. Per RFC 8308, some
+ // servers may not announce _all_ their supported algorithms,
+ // and a client may use unknown algorithms.
+ clientAlgorithms.addAll(unknown);
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "handleServerSignatureAlgorithms({}): PubkeyAcceptedAlgorithms after: {}", //$NON-NLS-1$
+ session, clientAlgorithms);
+ }
+ session.setAttribute(SERVER_ALGORITHMS, known);
+ session.setSignatureFactories(clientAlgorithms);
+ }
+ }
+}
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
index 297b456..6755094 100644
--- 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
@@ -11,6 +11,9 @@
import java.io.IOException;
import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.text.MessageFormat;
+import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@@ -19,6 +22,7 @@
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.NamedResource;
import org.apache.sshd.common.RuntimeSshException;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.config.keys.KeyUtils;
@@ -37,7 +41,9 @@
*/
public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
- private final List<String> algorithms = new LinkedList<>();
+ private final Deque<String> currentAlgorithms = new LinkedList<>();
+
+ private String chosenAlgorithm;
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
@@ -47,11 +53,25 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
protected boolean sendAuthDataRequest(ClientSession session, String service)
throws Exception {
if (current == null) {
- algorithms.clear();
+ currentAlgorithms.clear();
+ chosenAlgorithm = null;
}
String currentAlgorithm = null;
- if (current != null && !algorithms.isEmpty()) {
- currentAlgorithm = algorithms.remove(0);
+ if (current != null && !currentAlgorithms.isEmpty()) {
+ currentAlgorithm = currentAlgorithms.poll();
+ if (chosenAlgorithm != null) {
+ Set<String> knownServerAlgorithms = session.getAttribute(
+ JGitKexExtensionHandler.SERVER_ALGORITHMS);
+ if (knownServerAlgorithms != null
+ && knownServerAlgorithms.contains(chosenAlgorithm)) {
+ // We've tried key 'current' with 'chosenAlgorithm', but it
+ // failed. However, the server had told us it supported
+ // 'chosenAlgorithm'. Thus it makes no sense to continue
+ // with this key and other signature algorithms. Skip to the
+ // next key, if any.
+ currentAlgorithm = null;
+ }
+ }
}
if (currentAlgorithm == null) {
try {
@@ -61,14 +81,13 @@ protected boolean sendAuthDataRequest(ClientSession session, String service)
"sendAuthDataRequest({})[{}] no more keys to send", //$NON-NLS-1$
session, service);
}
+ current = null;
return false;
}
current = keys.next();
- algorithms.clear();
+ currentAlgorithms.clear();
+ chosenAlgorithm = null;
} 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);
}
}
@@ -76,9 +95,6 @@ protected boolean sendAuthDataRequest(ClientSession session, String service)
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) {
@@ -94,15 +110,21 @@ protected boolean sendAuthDataRequest(ClientSession session, String service)
existingFactories = getSignatureFactories();
}
if (existingFactories != null) {
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "sendAuthDataRequest({})[{}] selecting from PubKeyAcceptedAlgorithms {}", //$NON-NLS-1$
+ session, service,
+ NamedResource.getNames(existingFactories));
+ }
// Select the factories by name and in order
existingFactories.forEach(f -> {
if (aliases.contains(f.getName())) {
- algorithms.add(f.getName());
+ currentAlgorithms.add(f.getName());
}
});
}
- currentAlgorithm = algorithms.isEmpty() ? keyType
- : algorithms.remove(0);
+ currentAlgorithm = currentAlgorithms.isEmpty() ? keyType
+ : currentAlgorithms.poll();
}
String name = getName();
if (log.isDebugEnabled()) {
@@ -112,6 +134,7 @@ protected boolean sendAuthDataRequest(ClientSession session, String service)
KeyUtils.getFingerPrint(key));
}
+ chosenAlgorithm = currentAlgorithm;
Buffer buffer = session
.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
buffer.putString(session.getUsername());
@@ -125,9 +148,77 @@ protected boolean sendAuthDataRequest(ClientSession session, String service)
}
@Override
+ protected boolean processAuthDataRequest(ClientSession session,
+ String service, Buffer buffer) throws Exception {
+ String name = getName();
+ int cmd = buffer.getUByte();
+ if (cmd != SshConstants.SSH_MSG_USERAUTH_PK_OK) {
+ throw new IllegalStateException(MessageFormat.format(
+ SshdText.get().pubkeyAuthWrongCommand,
+ SshConstants.getCommandMessageName(cmd),
+ session.getConnectAddress(), session.getServerVersion()));
+ }
+ PublicKey key;
+ try {
+ key = current.getPublicKey();
+ } catch (Error e) { // Copied from superclass
+ throw new RuntimeSshException(e);
+ }
+ String rspKeyAlgorithm = buffer.getString();
+ PublicKey rspKey = buffer.getPublicKey();
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "processAuthDataRequest({})[{}][{}] SSH_MSG_USERAUTH_PK_OK type={}, fingerprint={}", //$NON-NLS-1$
+ session, service, name, rspKeyAlgorithm,
+ KeyUtils.getFingerPrint(rspKey));
+ }
+ if (!KeyUtils.compareKeys(rspKey, key)) {
+ throw new InvalidKeySpecException(MessageFormat.format(
+ SshdText.get().pubkeyAuthWrongKey,
+ KeyUtils.getFingerPrint(key),
+ KeyUtils.getFingerPrint(rspKey),
+ session.getConnectAddress(), session.getServerVersion()));
+ }
+ if (!chosenAlgorithm.equalsIgnoreCase(rspKeyAlgorithm)) {
+ // 'algo' SHOULD be the same as 'chosenAlgorithm', which is the one
+ // we sent above. See https://tools.ietf.org/html/rfc4252#page-9 .
+ //
+ // However, at least Github (SSH-2.0-babeld-383743ad) servers seem
+ // to return the key type, not the algorithm name.
+ //
+ // So we don't check but just log the inconsistency. We sign using
+ // 'chosenAlgorithm' in any case, so we don't really care what the
+ // server says here.
+ log.warn(MessageFormat.format(
+ SshdText.get().pubkeyAuthWrongSignatureAlgorithm,
+ chosenAlgorithm, rspKeyAlgorithm, session.getConnectAddress(),
+ session.getServerVersion()));
+ }
+ String username = session.getUsername();
+ Buffer out = session
+ .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
+ out.putString(username);
+ out.putString(service);
+ out.putString(name);
+ out.putBoolean(true);
+ out.putString(chosenAlgorithm);
+ out.putPublicKey(key);
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "processAuthDataRequest({})[{}][{}]: signing with algorithm {}", //$NON-NLS-1$
+ session, service, name, chosenAlgorithm);
+ }
+ appendSignature(session, service, name, username, chosenAlgorithm, key,
+ out);
+ session.writePacket(out);
+ return true;
+ }
+
+ @Override
protected void releaseKeys() throws IOException {
- algorithms.clear();
+ currentAlgorithms.clear();
current = null;
+ chosenAlgorithm = null;
super.releaseKeys();
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
index 47e09b7..1a530b7 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
@@ -21,6 +21,7 @@
import java.net.SocketAddress;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
@@ -561,29 +562,17 @@ public HostKeyFile(Path path) {
@Override
public List<HostEntryPair> get() {
Path path = getPath();
- try {
- if (checkReloadRequired()) {
- if (!Files.exists(path)) {
- // Has disappeared.
- resetReloadAttributes();
- return Collections.emptyList();
+ synchronized (this) {
+ try {
+ if (checkReloadRequired()) {
+ entries = reload(getPath());
}
- LockFile lock = new LockFile(path.toFile());
- if (lock.lock()) {
- try {
- entries = reload(getPath());
- } finally {
- lock.unlock();
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
- path));
- }
+ } catch (IOException e) {
+ LOG.warn(format(SshdText.get().knownHostsFileReadFailed,
+ path));
}
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
+ return Collections.unmodifiableList(entries);
}
- return Collections.unmodifiableList(entries);
}
private List<HostEntryPair> reload(Path path) throws IOException {
@@ -616,7 +605,7 @@ private List<HostEntryPair> reload(Path path) throws IOException {
}
}
return newEntries;
- } catch (FileNotFoundException e) {
+ } catch (FileNotFoundException | NoSuchFileException e) {
resetReloadAttributes();
return Collections.emptyList();
}
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 4c4ff59..73c2288 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
@@ -44,7 +44,6 @@ public static SshdText get() {
/***/ public String keyEncryptedRetry;
/***/ public String keyLoadFailed;
/***/ public String knownHostsCouldNotUpdate;
- /***/ public String knownHostsFileLockedRead;
/***/ public String knownHostsFileLockedUpdate;
/***/ public String knownHostsFileReadFailed;
/***/ public String knownHostsInvalidLine;
@@ -88,6 +87,9 @@ public static SshdText get() {
/***/ public String proxySocksUnexpectedMessage;
/***/ public String proxySocksUnexpectedVersion;
/***/ public String proxySocksUsernameTooLong;
+ /***/ public String pubkeyAuthWrongCommand;
+ /***/ public String pubkeyAuthWrongKey;
+ /***/ public String pubkeyAuthWrongSignatureAlgorithm;
/***/ public String serverIdNotReceived;
/***/ public String serverIdTooLong;
/***/ public String serverIdWithNul;
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 cad959c..2d7e0c7 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
@@ -47,6 +47,7 @@
import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
+import org.eclipse.jgit.internal.transport.sshd.JGitKexExtensionHandler;
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier;
@@ -216,6 +217,7 @@ public SshdSession getSession(URIish uri,
new JGitUserInteraction(credentialsProvider));
client.setUserAuthFactories(getUserAuthFactories());
client.setKeyIdentityProvider(defaultKeysProvider);
+ client.setKexExtensionHandler(JGitKexExtensionHandler.INSTANCE);
// JGit-specific things:
JGitSshClient jgitClient = (JGitSshClient) client;
jgitClient.setKeyCache(getKeyCache());
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 4c7e99e..4be2271 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
@@ -58,6 +58,7 @@ public void setUp() throws Exception {
FileUtils.mkdir(configFile.getParentFile());
mockSystemReader.setProperty(Constants.OS_USER_NAME_KEY, "jex_junit");
+ mockSystemReader.setProperty("TST_VAR", "TEST");
osc = new OpenSshConfig(home, configFile);
}
@@ -497,4 +498,23 @@ public void testEolComments() throws Exception {
assertEquals("^ssh-rsa",
c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
}
+
+ @Test
+ public void testEnVarSubstitution() throws Exception {
+ config("Host orcz\nIdentityFile /tmp/${TST_VAR}\n"
+ + "CertificateFile /tmp/${}/foo\nUser ${TST_VAR}\nIdentityAgent /tmp/${TST_VAR/bar");
+ Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ Config c = h.getConfig();
+ assertEquals("/tmp/TEST",
+ c.getValue(SshConstants.IDENTITY_FILE));
+ // No variable name
+ assertEquals("/tmp/${}/foo", c.getValue(SshConstants.CERTIFICATE_FILE));
+ // User doesn't get env var substitution:
+ assertEquals("${TST_VAR}", c.getValue(SshConstants.USER));
+ assertEquals("${TST_VAR}", h.getUser());
+ // Unterminated:
+ assertEquals("/tmp/${TST_VAR/bar",
+ c.getValue(SshConstants.IDENTITY_AGENT));
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index eecf25b..6cbb4a8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -1648,6 +1648,82 @@ public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFil
indexState(CONTENT));
}
+ /**
+ * Merging two commits when files have equal content, but conflicting file mode
+ * in the virtual ancestor.
+ *
+ * <p>
+ * This test has the same set up as
+ * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
+ * with the mode conflict in A1 and A2.
+ */
+ @Theory
+ public void checkModeMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("c", "initial file");
+ git.add().addFilepattern("c").call();
+ RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+ File a = writeTrashFile("a", "content in Ancestor");
+ git.add().addFilepattern("a").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ a = writeTrashFile("a", "content in Child 1 (commited on master)");
+ git.add().addFilepattern("a").call();
+ // commit C1M
+ git.commit().setMessage("Child 1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+ // "a" becomes executable in A2
+ a = writeTrashFile("a", "content in Ancestor");
+ a.setExecutable(true);
+ git.add().addFilepattern("a").call();
+ RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+ // second branch
+ git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+ a = writeTrashFile("a", "content in Child 2 (commited on second-branch)");
+ git.add().addFilepattern("a").call();
+ // commit C2S
+ git.commit().setMessage("Child 2 on second-branch").call();
+
+ // Merge branch-to-merge into second-branch
+ MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+ assertEquals(mergeResult.getNewHead(), null);
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+ // Resolve the conflict manually, merge "a" as non-executable
+ a = writeTrashFile("a", "merge conflict resolution");
+ a.setExecutable(false);
+ git.add().addFilepattern("a").call();
+ RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
+
+ // Merge branch-to-merge into master
+ git.checkout().setName("master").call();
+ mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+ assertEquals(mergeResult.getNewHead(), null);
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+ // Resolve the conflict manually - merge "a" as non-executable
+ a = writeTrashFile("a", "merge conflict resolution");
+ a.setExecutable(false);
+ git.add().addFilepattern("a").call();
+ // commit C4M
+ git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+ // Merge C4M (second-branch) into master (C3S)
+ // Conflict in virtual base should be here, but there are no conflicts in
+ // children
+ mergeResult = git.merge().include(commitC3S).call();
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+
+ }
+
private void writeSubmodule(String path, ObjectId commit)
throws IOException, ConfigInvalidException {
addSubmoduleToIndex(path, commit);
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 c514270..de6a346 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
@@ -708,10 +708,10 @@ void merge(HostEntry entry) {
}
private List<String> substitute(List<String> values, String allowed,
- Replacer r) {
+ Replacer r, boolean withEnv) {
List<String> result = new ArrayList<>(values.size());
for (String value : values) {
- result.add(r.substitute(value, allowed));
+ result.add(r.substitute(value, allowed, withEnv));
}
return result;
}
@@ -743,7 +743,7 @@ void substitute(String originalHostName, int port, String userName,
if (hostName == null || hostName.isEmpty()) {
options.put(SshConstants.HOST_NAME, originalHostName);
} else {
- hostName = r.substitute(hostName, "h"); //$NON-NLS-1$
+ hostName = r.substitute(hostName, "h", false); //$NON-NLS-1$
options.put(SshConstants.HOST_NAME, hostName);
r.update('h', hostName);
}
@@ -752,13 +752,13 @@ void substitute(String originalHostName, int port, String userName,
List<String> values = multiOptions
.get(SshConstants.IDENTITY_FILE);
if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
values = replaceTilde(values, home);
multiOptions.put(SshConstants.IDENTITY_FILE, values);
}
values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
values = replaceTilde(values, home);
multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
}
@@ -775,29 +775,29 @@ void substitute(String originalHostName, int port, String userName,
// HOSTNAME already done above
String value = options.get(SshConstants.IDENTITY_AGENT);
if (value != null) {
- value = r.substitute(value, "dhlru"); //$NON-NLS-1$
+ value = r.substitute(value, "dhlru", true); //$NON-NLS-1$
value = toFile(value, home).getPath();
options.put(SshConstants.IDENTITY_AGENT, value);
}
value = options.get(SshConstants.CONTROL_PATH);
if (value != null) {
- value = r.substitute(value, "ChLlnpru"); //$NON-NLS-1$
+ value = r.substitute(value, "ChLlnpru", true); //$NON-NLS-1$
value = toFile(value, home).getPath();
options.put(SshConstants.CONTROL_PATH, value);
}
value = options.get(SshConstants.LOCAL_COMMAND);
if (value != null) {
- value = r.substitute(value, "CdhlnprTu"); //$NON-NLS-1$
+ value = r.substitute(value, "CdhlnprTu", false); //$NON-NLS-1$
options.put(SshConstants.LOCAL_COMMAND, value);
}
value = options.get(SshConstants.REMOTE_COMMAND);
if (value != null) {
- value = r.substitute(value, "Cdhlnpru"); //$NON-NLS-1$
+ value = r.substitute(value, "Cdhlnpru", false); //$NON-NLS-1$
options.put(SshConstants.REMOTE_COMMAND, value);
}
value = options.get(SshConstants.PROXY_COMMAND);
if (value != null) {
- value = r.substitute(value, "hpr"); //$NON-NLS-1$
+ value = r.substitute(value, "hpr", false); //$NON-NLS-1$
options.put(SshConstants.PROXY_COMMAND, value);
}
}
@@ -871,7 +871,7 @@ public Replacer(String host, int port, String user,
replacements.put(Character.valueOf('r'), user == null ? "" : user); //$NON-NLS-1$
replacements.put(Character.valueOf('u'), localUserName);
replacements.put(Character.valueOf('C'),
- substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
+ substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
replacements.put(Character.valueOf('T'), "NONE"); //$NON-NLS-1$
}
@@ -879,36 +879,63 @@ public void update(char key, String value) {
replacements.put(Character.valueOf(key), value);
if ("lhpr".indexOf(key) >= 0) { //$NON-NLS-1$
replacements.put(Character.valueOf('C'),
- substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
+ substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
- public String substitute(String input, String allowed) {
+ public String substitute(String input, String allowed,
+ boolean withEnv) {
if (input == null || input.length() <= 1
- || input.indexOf('%') < 0) {
+ || input.indexOf('%') < 0
+ && (!withEnv || input.indexOf("${") < 0)) { //$NON-NLS-1$
return input;
}
StringBuilder builder = new StringBuilder();
int start = 0;
int length = input.length();
while (start < length) {
- int percent = input.indexOf('%', start);
- if (percent < 0 || percent + 1 >= length) {
- builder.append(input.substring(start));
+ char ch = input.charAt(start);
+ switch (ch) {
+ case '%':
+ if (start + 1 >= length) {
+ break;
+ }
+ String replacement = null;
+ ch = input.charAt(start + 1);
+ if (ch == '%' || allowed.indexOf(ch) >= 0) {
+ replacement = replacements.get(Character.valueOf(ch));
+ }
+ if (replacement == null) {
+ builder.append('%').append(ch);
+ } else {
+ builder.append(replacement);
+ }
+ start += 2;
+ continue;
+ case '$':
+ if (!withEnv || start + 2 >= length) {
+ break;
+ }
+ ch = input.charAt(start + 1);
+ if (ch == '{') {
+ int close = input.indexOf('}', start + 2);
+ if (close > start + 2) {
+ String variable = SystemReader.getInstance()
+ .getenv(input.substring(start + 2, close));
+ if (!StringUtils.isEmptyOrNull(variable)) {
+ builder.append(variable);
+ }
+ start = close + 1;
+ continue;
+ }
+ }
+ ch = '$';
+ break;
+ default:
break;
}
- String replacement = null;
- char ch = input.charAt(percent + 1);
- if (ch == '%' || allowed.indexOf(ch) >= 0) {
- replacement = replacements.get(Character.valueOf(ch));
- }
- if (replacement == null) {
- builder.append(input.substring(start, percent + 2));
- } else {
- builder.append(input.substring(start, percent))
- .append(replacement);
- }
- start = percent + 2;
+ builder.append(ch);
+ start++;
}
return builder.toString();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 4bfb38d..b011258 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -644,15 +644,18 @@ protected boolean processEntry(CanonicalTreeParser base,
}
return true;
}
- // FileModes are not mergeable. We found a conflict on modes.
- // For conflicting entries we don't know lastModified and
- // length.
- add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
- add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
- add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
- unmergedPaths.add(tw.getPathString());
- mergeResults.put(tw.getPathString(),
- new MergeResult<>(Collections.<RawText> emptyList()));
+ if (!ignoreConflicts) {
+ // FileModes are not mergeable. We found a conflict on modes.
+ // For conflicting entries we don't know lastModified and
+ // length.
+ // This path can be skipped on ignoreConflicts, so the caller
+ // could use virtual commit.
+ add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+ add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+ add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+ unmergedPaths.add(tw.getPathString());
+ mergeResults.put(tw.getPathString(), new MergeResult<>(Collections.<RawText>emptyList()));
+ }
return true;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
index 979961f..c0de42c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010, Google Inc. and others
+ * Copyright (C) 2010, 2021 Google Inc. 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
@@ -58,14 +58,21 @@ public boolean isInteractive() {
@Override
public boolean supports(CredentialItem... items) {
for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username)
+ if (i instanceof CredentialItem.InformationalMessage) {
continue;
-
- else if (i instanceof CredentialItem.Password)
+ }
+ if (i instanceof CredentialItem.Username) {
continue;
-
- else
- return false;
+ }
+ if (i instanceof CredentialItem.Password) {
+ continue;
+ }
+ if (i instanceof CredentialItem.StringType) {
+ if (i.getPromptText().equals("Password: ")) { //$NON-NLS-1$
+ continue;
+ }
+ }
+ return false;
}
return true;
}
@@ -75,6 +82,9 @@ else if (i instanceof CredentialItem.Password)
public boolean get(URIish uri, CredentialItem... items)
throws UnsupportedCredentialItem {
for (CredentialItem i : items) {
+ if (i instanceof CredentialItem.InformationalMessage) {
+ continue;
+ }
if (i instanceof CredentialItem.Username) {
((CredentialItem.Username) i).setValue(username);
continue;