+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.net.InetSocketAddress;
+import java.security.PublicKey;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * An interface for a database of known server keys, supporting finding all
+ * known keys and also deciding whether a server key is to be accepted.
+ *
+ * Connection addresses are given as strings of the format
+ * {@code [hostName]:port} if using a non-standard port (i.e., not port 22),
+ * otherwise just {@code hostname}.
+ *
+ *
+ * @since 5.5
+ */
+public interface ServerKeyDatabase {
+
+ /**
+ * Retrieves all known host keys for the given addresses.
+ *
+ * @param connectAddress
+ * IP address the session tried to connect to
+ * @param remoteAddress
+ * IP address as reported for the remote end point
+ * @param config
+ * giving access to potentially interesting configuration
+ * settings
+ * @return the list of known keys for the given addresses
+ */
+ @NonNull
+ List lookup(@NonNull String connectAddress,
+ @NonNull InetSocketAddress remoteAddress,
+ @NonNull Configuration config);
+
+ /**
+ * Determines whether to accept a received server host key.
+ *
+ * @param connectAddress
+ * IP address the session tried to connect to
+ * @param remoteAddress
+ * IP address as reported for the remote end point
+ * @param serverKey
+ * received from the remote end
+ * @param config
+ * giving access to potentially interesting configuration
+ * settings
+ * @param provider
+ * for interacting with the user, if required; may be
+ * {@code null}
+ * @return {@code true} if the serverKey is accepted, {@code false}
+ * otherwise
+ */
+ boolean accept(@NonNull String connectAddress,
+ @NonNull InetSocketAddress remoteAddress,
+ @NonNull PublicKey serverKey,
+ @NonNull Configuration config, CredentialsProvider provider);
+
+ /**
+ * A simple provider for ssh config settings related to host key checking.
+ * An instance is created by the JGit sshd framework and passed into
+ * {@link ServerKeyDatabase#lookup(String, InetSocketAddress, Configuration)}
+ * and
+ * {@link ServerKeyDatabase#accept(String, InetSocketAddress, PublicKey, Configuration, CredentialsProvider)}.
+ */
+ interface Configuration {
+
+ /**
+ * Retrieves the list of file names from the "UserKnownHostsFile" ssh
+ * config.
+ *
+ * @return the list as configured, with ~ already replaced
+ */
+ List getUserKnownHostsFiles();
+
+ /**
+ * Retrieves the list of file names from the "GlobalKnownHostsFile" ssh
+ * config.
+ *
+ * @return the list as configured, with ~ already replaced
+ */
+ List getGlobalKnownHostsFiles();
+
+ /**
+ * The possible values for the "StrictHostKeyChecking" ssh config.
+ */
+ enum StrictHostKeyChecking {
+ /**
+ * "ask"; default: ask the user whether to accept (and store) a new
+ * or mismatched key.
+ */
+ ASK,
+ /**
+ * "yes", "on": never accept new or mismatched keys.
+ */
+ REQUIRE_MATCH,
+ /**
+ * "no", "off": always accept new or mismatched keys.
+ */
+ ACCEPT_ANY,
+ /**
+ * "accept-new": accept new keys, but never accept modified keys.
+ */
+ ACCEPT_NEW
+ }
+
+ /**
+ * Obtains the value of the "StrictHostKeyChecking" ssh config.
+ *
+ * @return the {@link StrictHostKeyChecking}
+ */
+ @NonNull
+ StrictHostKeyChecking getStrictHostKeyChecking();
+
+ /**
+ * Obtains the value of the "HashKnownHosts" ssh config.
+ *
+ * @return {@code true} if new entries should be stored with hashed host
+ * information, {@code false} otherwise
+ */
+ boolean getHashKnownHosts();
+
+ /**
+ * Obtains the user name used in the connection attempt.
+ *
+ * @return the user name
+ */
+ @NonNull
+ String getUsername();
+ }
+}
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 90dc8ca50..3460185d8 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf
+ * Copyright (C) 2018, 2019 Thomas Wolf
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -66,7 +66,6 @@ import org.apache.sshd.client.auth.UserAuth;
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.keyverifier.ServerKeyVerifier;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.compression.BuiltinCompressions;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
@@ -77,10 +76,11 @@ import org.eclipse.jgit.errors.TransportException;
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.JGitServerKeyVerifier;
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction;
-import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyVerifier;
+import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.CredentialsProvider;
@@ -104,7 +104,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
private final Map defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
- private final Map defaultServerKeyVerifier = new ConcurrentHashMap<>();
+ private final Map defaultServerKeyDatabase = new ConcurrentHashMap<>();
private final Map> defaultKeys = new ConcurrentHashMap<>();
@@ -226,7 +226,8 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
.filePasswordProvider(
createFilePasswordProvider(passphrases))
.hostConfigEntryResolver(configFile)
- .serverKeyVerifier(getServerKeyVerifier(home, sshDir))
+ .serverKeyVerifier(new JGitServerKeyVerifier(
+ getServerKeyDatabase(home, sshDir)))
.compressionFactories(
new ArrayList<>(BuiltinCompressions.VALUES))
.build();
@@ -360,34 +361,48 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
@NonNull File homeDir, @NonNull File sshDir) {
return defaultHostConfigEntryResolver.computeIfAbsent(
new Tuple(new Object[] { homeDir, sshDir }),
- t -> new JGitSshConfig(homeDir,
- new File(sshDir, SshConstants.CONFIG),
+ t -> new JGitSshConfig(homeDir, getSshConfig(sshDir),
getLocalUserName()));
}
/**
- * Obtain a {@link ServerKeyVerifier} to read known_hosts files and to
- * verify server host keys. The default implementation returns a
- * {@link ServerKeyVerifier} that recognizes the two openssh standard files
- * {@code ~/.ssh/known_hosts} and {@code ~/.ssh/known_hosts2} as well as any
- * files configured via the {@code UserKnownHostsFile} option in the ssh
- * config file.
+ * Determines the ssh config file. The default implementation returns
+ * ~/.ssh/config. If the file does not exist and is created later it will be
+ * picked up. To not use a config file at all, return {@code null}.
+ *
+ * @param sshDir
+ * representing ~/.ssh/
+ * @return the file (need not exist), or {@code null} if no config file
+ * shall be used
+ * @since 5.5
+ */
+ protected File getSshConfig(@NonNull File sshDir) {
+ return new File(sshDir, SshConstants.CONFIG);
+ }
+
+ /**
+ * Obtain a {@link ServerKeyDatabase} to verify server host keys. The
+ * default implementation returns a {@link ServerKeyDatabase} that
+ * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
+ * {@code ~/.ssh/known_hosts2} as well as any files configured via the
+ * {@code UserKnownHostsFile} option in the ssh config file.
*
* @param homeDir
* home directory to use for ~ replacement
* @param sshDir
* representing ~/.ssh/
- * @return the resolver
+ * @return the {@link ServerKeyDatabase}
+ * @since 5.5
*/
@NonNull
- private ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir,
+ protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
@NonNull File sshDir) {
- return defaultServerKeyVerifier.computeIfAbsent(
+ return defaultServerKeyDatabase.computeIfAbsent(
new Tuple(new Object[] { homeDir, sshDir }),
- t -> new OpenSshServerKeyVerifier(true,
+ t -> new OpenSshServerKeyDatabase(true,
getDefaultKnownHostsFiles(sshDir)));
- }
+ }
/**
* Gets the list of default user known hosts files. The default returns
* ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
@@ -540,7 +555,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* the ssh config defines {@code PreferredAuthentications} the value from
* the ssh config takes precedence.
*
- * @return a comma-separated list of algorithm names, or {@code null} if
+ * @return a comma-separated list of mechanism names, or {@code null} if
* none
*/
protected String getDefaultPreferredAuthentications() {
diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
index b8c90b2a4..dab93fd07 100644
--- a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
+++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
@@ -305,7 +305,7 @@ public abstract class SshTestBase extends SshTestHarness {
// without provider. If it works, the server host key was written
// correctly.
File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
- cloneWith("ssh://localhost/doesntmatter", clonedAgain, provider, //
+ cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
"Host localhost", //
"HostName localhost", //
"Port " + testPort, //
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
index 501b788f8..79cf4d5af 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
@@ -43,6 +43,7 @@
package org.eclipse.jgit.internal.storage.file;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.LOCK_FAILURE;
@@ -64,6 +65,7 @@ import static org.junit.Assume.assumeTrue;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -161,6 +163,33 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase {
refsChangedEvents = 0;
}
+ @Test
+ public void packedRefsFileIsSorted() throws IOException {
+ assumeTrue(atomic);
+
+ for (int i = 0; i < 2; i++) {
+ BatchRefUpdate bu = diskRepo.getRefDatabase().newBatchUpdate();
+ String b1 = String.format("refs/heads/a%d",i);
+ String b2 = String.format("refs/heads/b%d",i);
+ bu.setAtomic(atomic);
+ ReceiveCommand c1 = new ReceiveCommand(ObjectId.zeroId(), A, b1);
+ ReceiveCommand c2 = new ReceiveCommand(ObjectId.zeroId(), B, b2);
+ bu.addCommand(c1, c2);
+ try (RevWalk rw = new RevWalk(diskRepo)) {
+ bu.execute(rw, NullProgressMonitor.INSTANCE);
+ }
+ assertEquals(c1.getResult(), ReceiveCommand.Result.OK);
+ assertEquals(c2.getResult(), ReceiveCommand.Result.OK);
+ }
+
+ File packed = new File(diskRepo.getDirectory(), "packed-refs");
+ String packedStr = new String(Files.readAllBytes(packed.toPath()), UTF_8);
+
+ int a2 = packedStr.indexOf("refs/heads/a1");
+ int b1 = packedStr.indexOf("refs/heads/b0");
+ assertTrue(a2 < b1);
+ }
+
@Test
public void simpleNoForce() throws IOException {
writeLooseRef("refs/heads/master", A);
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index f50ea9a6d..e4d023d17 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,166 +1,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
index 200c63c17..e45b53ea6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
@@ -51,10 +51,10 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_RE
import java.io.IOException;
import java.text.MessageFormat;
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -364,65 +364,72 @@ class PackedBatchRefUpdate extends BatchRefUpdate {
private static RefList[ applyUpdates(RevWalk walk, RefList][ refs,
List commands) throws IOException {
- int nDeletes = 0;
- List adds = new ArrayList<>(commands.size());
+ // Construct a new RefList by merging the old list with the updates.
+ // This assumes that each ref occurs at most once as a ReceiveCommand.
+ Collections.sort(commands, new Comparator() {
+ @Override
+ public int compare(ReceiveCommand a, ReceiveCommand b) {
+ return a.getRefName().compareTo(b.getRefName());
+ }
+ });
+
+ int delta = 0;
for (ReceiveCommand c : commands) {
- if (c.getType() == ReceiveCommand.Type.CREATE) {
- adds.add(c);
- } else if (c.getType() == ReceiveCommand.Type.DELETE) {
- nDeletes++;
+ switch (c.getType()) {
+ case DELETE:
+ delta--;
+ break;
+ case CREATE:
+ delta++;
+ break;
+ default:
}
}
- int addIdx = 0;
-
- // Construct a new RefList by linearly scanning the old list, and merging in
- // any updates.
- Map byName = byName(commands);
- RefList.Builder][ b =
- new RefList.Builder<>(refs.size() - nDeletes + adds.size());
- for (Ref ref : refs) {
- String name = ref.getName();
- ReceiveCommand cmd = byName.remove(name);
- if (cmd == null) {
- b.add(ref);
- continue;
- }
- if (!cmd.getOldId().equals(ref.getObjectId())) {
- lockFailure(cmd, commands);
- return null;
+
+ RefList.Builder][ b = new RefList.Builder<>(refs.size() + delta);
+ int refIdx = 0;
+ int cmdIdx = 0;
+ while (refIdx < refs.size() || cmdIdx < commands.size()) {
+ Ref ref = (refIdx < refs.size()) ? refs.get(refIdx) : null;
+ ReceiveCommand cmd = (cmdIdx < commands.size())
+ ? commands.get(cmdIdx)
+ : null;
+ int cmp = 0;
+ if (ref != null && cmd != null) {
+ cmp = ref.getName().compareTo(cmd.getRefName());
+ } else if (ref == null) {
+ cmp = 1;
+ } else if (cmd == null) {
+ cmp = -1;
}
- // Consume any adds between the last and current ref.
- while (addIdx < adds.size()) {
- ReceiveCommand currAdd = adds.get(addIdx);
- if (currAdd.getRefName().compareTo(name) < 0) {
- b.add(peeledRef(walk, currAdd));
- byName.remove(currAdd.getRefName());
- } else {
- break;
+ if (cmp < 0) {
+ b.add(ref);
+ refIdx++;
+ } else if (cmp > 0) {
+ assert cmd != null;
+ if (cmd.getType() != ReceiveCommand.Type.CREATE) {
+ lockFailure(cmd, commands);
+ return null;
}
- addIdx++;
- }
- if (cmd.getType() != ReceiveCommand.Type.DELETE) {
b.add(peeledRef(walk, cmd));
- }
- }
-
- // All remaining adds are valid, since the refs didn't exist.
- while (addIdx < adds.size()) {
- ReceiveCommand cmd = adds.get(addIdx++);
- byName.remove(cmd.getRefName());
- b.add(peeledRef(walk, cmd));
- }
+ cmdIdx++;
+ } else {
+ assert cmd != null;
+ assert ref != null;
+ if (!cmd.getOldId().equals(ref.getObjectId())) {
+ lockFailure(cmd, commands);
+ return null;
+ }
- // Any remaining updates/deletes do not correspond to any existing refs, so
- // they are lock failures.
- if (!byName.isEmpty()) {
- lockFailure(byName.values().iterator().next(), commands);
- return null;
+ if (cmd.getType() != ReceiveCommand.Type.DELETE) {
+ b.add(peeledRef(walk, cmd));
+ }
+ cmdIdx++;
+ refIdx++;
+ }
}
-
return b.toRefList();
}
@@ -501,15 +508,6 @@ class PackedBatchRefUpdate extends BatchRefUpdate {
}
}
- private static Map byName(
- List commands) {
- Map ret = new LinkedHashMap<>();
- for (ReceiveCommand cmd : commands) {
- ret.put(cmd.getRefName(), cmd);
- }
- return ret;
- }
-
private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
throws IOException {
ObjectId newId = cmd.getNewId().copy();
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 2b79d7105..efbe77704 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
@@ -101,6 +101,13 @@ public final class SshConstants {
/** Key in an ssh config file. */
public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile";
+ /**
+ * Key in an ssh config file.
+ *
+ * @since 5.5
+ */
+ public static final String HASH_KNOWN_HOSTS = "HashKnownHosts";
+
/** Key in an ssh config file. */
public static final String HOST = "Host";
]