diff --git a/WORKSPACE b/WORKSPACE index 97bf81e27..c5e4ef836 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,7 +15,7 @@ versions.check(minimum_bazel_version = "0.26.1") load("//tools:bazlets.bzl", "load_bazlets") -load_bazlets(commit = "8528a0df69dadf6311d8d3f81c1b693afda8bcf1") +load_bazlets(commit = "09a035e98077dce549d5f6a7472d06c4b8f792d2") load( "@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", diff --git a/org.eclipse.jgit.lfs/.settings/.api_filters b/org.eclipse.jgit.lfs/.settings/.api_filters deleted file mode 100644 index 9747df8aa..000000000 --- a/org.eclipse.jgit.lfs/.settings/.api_filters +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml index 57ffc3f44..fb36ac0ab 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml @@ -18,8 +18,8 @@ - - + + - - + + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml index 104e837eb..4604b4f8a 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml @@ -18,8 +18,8 @@ - - + + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml index 003aa8d59..1b2897d04 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml @@ -18,8 +18,8 @@ - - + + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml index 33a738dd8..dd662c9f2 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml @@ -18,8 +18,8 @@ - - + + - - + + diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml index 8ff596075..2582a77e7 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml @@ -18,8 +18,8 @@ - - + + 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 b863bf3fe..4bae3d9b6 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -7,7 +7,15 @@ Bundle-Version: 5.6.0.qualifier Bundle-Vendor: %Bundle-Vendor Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.eclipse.jgit.api.errors;version="[5.6.0,5.7.0)", +Import-Package: org.apache.sshd.client.config.hosts;version="[2.2.0,2.3.0)", + org.apache.sshd.common;version="[2.2.0,2.3.0)", + org.apache.sshd.common.auth;version="[2.2.0,2.3.0)", + org.apache.sshd.common.config.keys;version="[2.2.0,2.3.0)", + org.apache.sshd.common.keyprovider;version="[2.2.0,2.3.0)", + org.apache.sshd.common.session;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.net;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.security;version="[2.2.0,2.3.0)", + org.eclipse.jgit.api.errors;version="[5.6.0,5.7.0)", org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.6.0,5.7.0)", org.eclipse.jgit.junit;version="[5.6.0,5.7.0)", org.eclipse.jgit.junit.ssh;version="[5.6.0,5.7.0)", 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 df0b832a0..b9b7353d3 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 @@ -42,16 +42,22 @@ */ package org.eclipse.jgit.transport.sshd; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.sshd.client.config.hosts.KnownHostEntry; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.ssh.SshTestBase; -import org.eclipse.jgit.transport.sshd.SshdSessionFactory; import org.eclipse.jgit.util.FS; import org.junit.Test; import org.junit.experimental.theories.Theories; @@ -101,6 +107,51 @@ public class ApacheSshTest extends SshTestBase { "IdentityFile " + privateKey1.getAbsolutePath()); } + @Test + public void testHashedKnownHosts() throws Exception { + assertTrue("Failed to delete known_hosts", knownHosts.delete()); + // The provider will answer "yes" to all questions, so we should be able + // to connect and end up with a new known_hosts file with the host key. + TestCredentialsProvider provider = new TestCredentialsProvider(); + cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, // + "HashKnownHosts yes", // + "Host localhost", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + List messages = provider.getLog(); + assertFalse("Expected user interaction", messages.isEmpty()); + assertEquals( + "Expected to be asked about the key, and the file creation", 2, + messages.size()); + assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists()); + // Let's clone again without provider. If it works, the server host key + // was written correctly. + File clonedAgain = new File(getTemporaryDirectory(), "cloned2"); + cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, // + "Host localhost", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + // Check that the first line contains neither "localhost" nor + // "127.0.0.1", but does contain the expected hash. + List lines = Files.readAllLines(knownHosts.toPath()).stream() + .filter(s -> s != null && s.length() >= 1 && s.charAt(0) != '#' + && !s.trim().isEmpty()) + .collect(Collectors.toList()); + assertEquals("Unexpected number of known_hosts lines", 1, lines.size()); + String line = lines.get(0); + assertFalse("Found host in line", line.contains("localhost")); + assertFalse("Found IP in line", line.contains("127.0.0.1")); + assertTrue("Hash not found", line.contains("|")); + KnownHostEntry entry = KnownHostEntry.parseKnownHostEntry(line); + assertTrue("Hash doesn't match localhost", + entry.isHostMatch("localhost", testPort) + || entry.isHostMatch("127.0.0.1", testPort)); + } + @Test public void testPreamble() throws Exception { // Test that the client can deal with strange lines being sent before diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java new file mode 100644 index 000000000..c0dc2cf20 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * 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 static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.ssh.SshTestHarness; +import org.eclipse.jgit.util.FS; +import org.junit.After; +import org.junit.Test; + +/** + * Test for using the SshdSessionFactory without files in ~/.ssh but with an + * in-memory setup. + */ +public class NoFilesSshTest extends SshTestHarness { + + + private PublicKey testServerKey; + + private KeyPair testUserKey; + + @Override + protected SshSessionFactory createSessionFactory() { + SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(), + null) { + + @Override + protected File getSshConfig(File dir) { + return null; + } + + @Override + protected ServerKeyDatabase getServerKeyDatabase(File homeDir, + File dir) { + return new ServerKeyDatabase() { + + @Override + public List lookup(String connectAddress, + InetSocketAddress remoteAddress, + Configuration config) { + return Collections.singletonList(testServerKey); + } + + @Override + public boolean accept(String connectAddress, + InetSocketAddress remoteAddress, + PublicKey serverKey, Configuration config, + CredentialsProvider provider) { + return KeyUtils.compareKeys(serverKey, testServerKey); + } + + }; + } + + @Override + protected Iterable getDefaultKeys(File dir) { + // This would work for this simple test case: + // return Collections.singletonList(testUserKey); + // But let's see if we can check the host and username that's used. + // For that, we need access to the sshd SessionContext: + return new KeyAuthenticator(); + } + + @Override + protected String getDefaultPreferredAuthentications() { + return "publickey"; + } + }; + + // The home directory is mocked at this point! + result.setHomeDirectory(FS.DETECTED.userHome()); + result.setSshDirectory(sshDir); + return result; + } + + private class KeyAuthenticator implements KeyIdentityProvider, Iterable { + + @Override + public Iterator iterator() { + // Should not be called. The use of the Iterable interface in + // SshdSessionFactory.getDefaultKeys() made sense in sshd 2.0.0, + // but sshd 2.2.0 added the SessionContext, which although good + // (without it we couldn't check here) breaks the Iterable analogy. + // But we're stuck now with that interface for getDefaultKeys, and + // so this override throwing an exception is unfortunately needed. + throw new UnsupportedOperationException(); + } + + @Override + public Iterable loadKeys(SessionContext session) + throws IOException, GeneralSecurityException { + if (!TEST_USER.equals(session.getUsername())) { + return Collections.emptyList(); + } + SshdSocketAddress remoteAddress = SshdSocketAddress + .toSshdSocketAddress(session.getRemoteAddress()); + switch (remoteAddress.getHostName()) { + case "localhost": + case "127.0.0.1": + return Collections.singletonList(testUserKey); + default: + return Collections.emptyList(); + } + } + } + + @After + public void cleanUp() { + testServerKey = null; + testUserKey = null; + } + + @Override + protected void installConfig(String... config) { + File configFile = new File(sshDir, Constants.CONFIG); + if (config != null) { + try { + Files.write(configFile.toPath(), Arrays.asList(config)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private KeyPair load(Path path) throws Exception { + try (InputStream in = Files.newInputStream(path)) { + return SecurityUtils + .loadKeyPairIdentities(null, + NamedResource.ofName(path.toString()), in, null) + .iterator().next(); + } + } + + @Test + public void testCloneWithBuiltInKeys() throws Exception { + // This test should fail unless our in-memory setup is taken: no + // known_hosts file, and a config that specifies a non-existing key. + File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); + copyTestResource("id_ed25519", newHostKey); + server.addHostKey(newHostKey.toPath(), true); + testServerKey = load(newHostKey.toPath()).getPublic(); + assertTrue(newHostKey.delete()); + testUserKey = load(privateKey1.getAbsoluteFile().toPath()); + assertNotNull(testServerKey); + assertNotNull(testUserKey); + cloneWith( + "ssh://" + TEST_USER + "@localhost:" + testPort + + "/doesntmatter", + new File(getTemporaryDirectory(), "cloned"), null, // + "Host localhost", // + "IdentityFile " + + new File(sshDir, "does_not_exist").getAbsolutePath()); + } + +} diff --git a/org.eclipse.jgit.ssh.apache/.settings/.api_filters b/org.eclipse.jgit.ssh.apache/.settings/.api_filters new file mode 100644 index 000000000..a0e469509 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/.settings/.api_filters @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 4ce4f6aad..1954abc75 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 @@ -57,7 +57,6 @@ import java.util.Set; import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.config.hosts.HostConfigEntry; -import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSessionImpl; import org.apache.sshd.common.FactoryManager; @@ -293,11 +292,10 @@ public class JGitClientSession extends ClientSessionImpl { if (verifier instanceof ServerKeyLookup) { SocketAddress remoteAddress = resolvePeerAddress( resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS)); - List allKnownKeys = ((ServerKeyLookup) verifier) + List allKnownKeys = ((ServerKeyLookup) verifier) .lookup(this, remoteAddress); Set reordered = new LinkedHashSet<>(); - for (HostEntryPair h : allKnownKeys) { - PublicKey key = h.getServerKey(); + for (PublicKey key : allKnownKeys) { if (key != null) { String keyType = KeyUtils.getKeyType(key); if (keyType != null) { diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java new file mode 100644 index 000000000..b94515c15 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * 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.internal.transport.sshd; + +import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.KnownHostHashValue; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A bridge between the {@link ServerKeyVerifier} from Apache MINA sshd and our + * {@link ServerKeyDatabase}. + */ +public class JGitServerKeyVerifier + implements ServerKeyVerifier, ServerKeyLookup { + + private static final Logger LOG = LoggerFactory + .getLogger(JGitServerKeyVerifier.class); + + private final @NonNull ServerKeyDatabase database; + + /** + * Creates a new {@link JGitServerKeyVerifier} using the given + * {@link ServerKeyDatabase}. + * + * @param database + * to use + */ + public JGitServerKeyVerifier(@NonNull ServerKeyDatabase database) { + this.database = database; + } + + @Override + public List lookup(ClientSession session, + SocketAddress remoteAddress) { + if (!(session instanceof JGitClientSession)) { + LOG.warn("Internal error: wrong session kind: " //$NON-NLS-1$ + + session.getClass().getName()); + return Collections.emptyList(); + } + if (!(remoteAddress instanceof InetSocketAddress)) { + return Collections.emptyList(); + } + SessionConfig config = new SessionConfig((JGitClientSession) session); + SshdSocketAddress connectAddress = SshdSocketAddress + .toSshdSocketAddress(session.getConnectAddress()); + String connect = KnownHostHashValue.createHostPattern( + connectAddress.getHostName(), connectAddress.getPort()); + return database.lookup(connect, (InetSocketAddress) remoteAddress, + config); + } + + @Override + public boolean verifyServerKey(ClientSession session, + SocketAddress remoteAddress, PublicKey serverKey) { + if (!(session instanceof JGitClientSession)) { + LOG.warn("Internal error: wrong session kind: " //$NON-NLS-1$ + + session.getClass().getName()); + return false; + } + if (!(remoteAddress instanceof InetSocketAddress)) { + return false; + } + SessionConfig config = new SessionConfig((JGitClientSession) session); + SshdSocketAddress connectAddress = SshdSocketAddress + .toSshdSocketAddress(session.getConnectAddress()); + String connect = KnownHostHashValue.createHostPattern( + connectAddress.getHostName(), connectAddress.getPort()); + CredentialsProvider provider = ((JGitClientSession) session) + .getCredentialsProvider(); + return database.accept(connect, (InetSocketAddress) remoteAddress, + serverKey, config, provider); + } + + private static class SessionConfig + implements ServerKeyDatabase.Configuration { + + private final JGitClientSession session; + + public SessionConfig(JGitClientSession session) { + this.session = session; + } + + private List get(String key) { + HostConfigEntry entry = session.getHostConfigEntry(); + if (entry instanceof JGitHostConfigEntry) { + // Always true! + return ((JGitHostConfigEntry) entry).getMultiValuedOptions() + .get(key); + } + return Collections.emptyList(); + } + + @Override + public List getUserKnownHostsFiles() { + return get(SshConstants.USER_KNOWN_HOSTS_FILE); + } + + @Override + public List getGlobalKnownHostsFiles() { + return get(SshConstants.GLOBAL_KNOWN_HOSTS_FILE); + } + + @Override + public StrictHostKeyChecking getStrictHostKeyChecking() { + HostConfigEntry entry = session.getHostConfigEntry(); + String value = entry + .getProperty(SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); //$NON-NLS-1$ + switch (value.toLowerCase(Locale.ROOT)) { + case SshConstants.YES: + case SshConstants.ON: + return StrictHostKeyChecking.REQUIRE_MATCH; + case SshConstants.NO: + case SshConstants.OFF: + return StrictHostKeyChecking.ACCEPT_ANY; + case "accept-new": //$NON-NLS-1$ + return StrictHostKeyChecking.ACCEPT_NEW; + default: + return StrictHostKeyChecking.ASK; + } + } + + @Override + public boolean getHashKnownHosts() { + HostConfigEntry entry = session.getHostConfigEntry(); + return flag(entry.getProperty(SshConstants.HASH_KNOWN_HOSTS)); + } + + @Override + public String getUsername() { + return session.getUsername(); + } + } +} 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 98e71dfe4..377eabb00 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 @@ -177,6 +177,10 @@ public class JGitSshClient extends SshClient { return remoteAddress; } InetSocketAddress address = (InetSocketAddress) proxy.address(); + if (address.isUnresolved()) { + address = new InetSocketAddress(address.getHostName(), + address.getPort()); + } switch (proxy.type()) { case HTTP: setClientProxyConnector( diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java index 6468b3e27..54a2a052a 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java @@ -83,7 +83,9 @@ import org.eclipse.jgit.transport.SshConstants; */ public class JGitSshConfig implements HostConfigEntryResolver { - private OpenSshConfigFile configFile; + private final OpenSshConfigFile configFile; + + private final String localUserName; /** * Creates a new {@link OpenSshConfigFile} that will read the config from @@ -92,20 +94,22 @@ public class JGitSshConfig implements HostConfigEntryResolver { * @param home * user's home directory for the purpose of ~ replacement * @param config - * file to load. + * file to load; may be {@code null} if no ssh config file + * handling is desired * @param localUserName * user name of the current user on the local host OS */ - public JGitSshConfig(@NonNull File home, @NonNull File config, + public JGitSshConfig(@NonNull File home, File config, @NonNull String localUserName) { - configFile = new OpenSshConfigFile(home, config, localUserName); + this.localUserName = localUserName; + configFile = config == null ? null : new OpenSshConfigFile(home, config, localUserName); } @Override public HostConfigEntry resolveEffectiveHost(String host, int port, SocketAddress localAddress, String username, AttributeRepository attributes) throws IOException { - HostEntry entry = configFile.lookup(host, port, username); + HostEntry entry = configFile == null ? new HostEntry() : configFile.lookup(host, port, username); JGitHostConfigEntry config = new JGitHostConfigEntry(); // Apache MINA conflates all keys, even multi-valued ones, in one map // and puts multiple values separated by commas in one string. See @@ -131,7 +135,7 @@ public class JGitSshConfig implements HostConfigEntryResolver { String user = username != null && !username.isEmpty() ? username : entry.getValue(SshConstants.USER); if (user == null || user.isEmpty()) { - user = configFile.getLocalUserName(); + user = localUserName; } config.setUsername(user); config.setProperty(SshConstants.USER, user); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java similarity index 67% rename from org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java rename to org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java index 381f7cfc2..f4849ce4a 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.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 @@ -47,7 +47,6 @@ import static java.text.MessageFormat.format; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStreamWriter; @@ -59,34 +58,39 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Map; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; -import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.HostPatternsHolder; +import org.apache.sshd.client.config.hosts.KnownHostDigest; import org.apache.sshd.client.config.hosts.KnownHostEntry; -import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; +import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; -import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntry; import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.util.io.ModifiableFileWatcher; import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -148,14 +152,14 @@ import org.slf4j.LoggerFactory; * @see man * ssh-config */ -public class OpenSshServerKeyVerifier - implements ServerKeyVerifier, ServerKeyLookup { +public class OpenSshServerKeyDatabase + implements ServerKeyDatabase { // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these // files may be large! private static final Logger LOG = LoggerFactory - .getLogger(OpenSshServerKeyVerifier.class); + .getLogger(OpenSshServerKeyDatabase.class); /** Can be used to mark revoked known host lines. */ private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$ @@ -166,12 +170,8 @@ public class OpenSshServerKeyVerifier private final List defaultFiles = new ArrayList<>(); - private enum ModifiedKeyHandling { - DENY, ALLOW, ALLOW_AND_STORE - } - /** - * Creates a new {@link OpenSshServerKeyVerifier}. + * Creates a new {@link OpenSshServerKeyDatabase}. * * @param askAboutNewFile * whether to ask the user, if possible, about creating a new @@ -181,7 +181,7 @@ public class OpenSshServerKeyVerifier * empty or {@code null}, in which case no default files are * installed. The files need not exist. */ - public OpenSshServerKeyVerifier(boolean askAboutNewFile, + public OpenSshServerKeyDatabase(boolean askAboutNewFile, List defaultFiles) { if (defaultFiles != null) { for (Path file : defaultFiles) { @@ -193,38 +193,30 @@ public class OpenSshServerKeyVerifier this.askAboutNewFile = askAboutNewFile; } - private List getFilesToUse(ClientSession session) { + private List getFilesToUse(@NonNull Configuration config) { List filesToUse = defaultFiles; - if (session instanceof JGitClientSession) { - HostConfigEntry entry = ((JGitClientSession) session) - .getHostConfigEntry(); - if (entry instanceof JGitHostConfigEntry) { - // Always true! - List userFiles = addUserHostKeyFiles( - ((JGitHostConfigEntry) entry).getMultiValuedOptions() - .get(SshConstants.USER_KNOWN_HOSTS_FILE)); - if (!userFiles.isEmpty()) { - filesToUse = userFiles; - } - } + List userFiles = addUserHostKeyFiles( + config.getUserKnownHostsFiles()); + if (!userFiles.isEmpty()) { + filesToUse = userFiles; } return filesToUse; } @Override - public List lookup(ClientSession session, - SocketAddress remote) { - List filesToUse = getFilesToUse(session); - HostKeyHelper helper = new HostKeyHelper(); - List result = new ArrayList<>(); - Collection candidates = helper - .resolveHostNetworkIdentities(session, remote); + public List lookup(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull Configuration config) { + List filesToUse = getFilesToUse(config); + List result = new ArrayList<>(); + Collection candidates = getCandidates( + connectAddress, remoteAddress); for (HostKeyFile file : filesToUse) { for (HostEntryPair current : file.get()) { KnownHostEntry entry = current.getHostEntry(); for (SshdSocketAddress host : candidates) { if (entry.isHostMatch(host.getHostName(), host.getPort())) { - result.add(current); + result.add(current.getServerKey()); break; } } @@ -234,22 +226,23 @@ public class OpenSshServerKeyVerifier } @Override - public boolean verifyServerKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey) { - List filesToUse = getFilesToUse(clientSession); - AskUser ask = new AskUser(); + public boolean accept(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull PublicKey serverKey, + @NonNull Configuration config, CredentialsProvider provider) { + List filesToUse = getFilesToUse(config); + AskUser ask = new AskUser(config, provider); HostEntryPair[] modified = { null }; Path path = null; - HostKeyHelper helper = new HostKeyHelper(); + Collection candidates = getCandidates(connectAddress, + remoteAddress); for (HostKeyFile file : filesToUse) { try { - if (find(clientSession, remoteAddress, serverKey, file.get(), - modified, helper)) { + if (find(candidates, serverKey, file.get(), modified)) { return true; } } catch (RevokedKeyException e) { - ask.revokedKey(clientSession, remoteAddress, serverKey, - file.getPath()); + ask.revokedKey(remoteAddress, serverKey, file.getPath()); return false; } if (path == null && modified[0] != null) { @@ -260,20 +253,19 @@ public class OpenSshServerKeyVerifier } if (modified[0] != null) { // We found an entry, but with a different key - ModifiedKeyHandling toDo = ask.acceptModifiedServerKey( - clientSession, remoteAddress, modified[0].getServerKey(), + AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey( + remoteAddress, modified[0].getServerKey(), serverKey, path); - if (toDo == ModifiedKeyHandling.ALLOW_AND_STORE) { + if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) { try { - updateModifiedServerKey(clientSession, remoteAddress, - serverKey, modified[0], path, helper); + updateModifiedServerKey(serverKey, modified[0], path); knownHostsFiles.get(path).resetReloadAttributes(); } catch (IOException e) { LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, path)); } } - if (toDo == ModifiedKeyHandling.DENY) { + if (toDo == AskUser.ModifiedKeyHandling.DENY) { return false; } // TODO: OpenSsh disables password and keyboard-interactive @@ -281,18 +273,20 @@ public class OpenSshServerKeyVerifier // are switched off. (Plus a few other things such as X11 forwarding // that are of no interest to a git client.) return true; - } else if (ask.acceptUnknownKey(clientSession, remoteAddress, - serverKey)) { + } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) { if (!filesToUse.isEmpty()) { HostKeyFile toUpdate = filesToUse.get(0); path = toUpdate.getPath(); try { - updateKnownHostsFile(clientSession, remoteAddress, - serverKey, path, helper); - toUpdate.resetReloadAttributes(); - } catch (IOException e) { + if (Files.exists(path) || !askAboutNewFile + || ask.createNewFile(path)) { + updateKnownHostsFile(candidates, serverKey, path, + config); + toUpdate.resetReloadAttributes(); + } + } catch (Exception e) { LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, - path)); + path), e); } } return true; @@ -304,12 +298,9 @@ public class OpenSshServerKeyVerifier private static final long serialVersionUID = 1L; } - private boolean find(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey, - List entries, HostEntryPair[] modified, - HostKeyHelper helper) throws RevokedKeyException { - Collection candidates = helper - .resolveHostNetworkIdentities(clientSession, remoteAddress); + private boolean find(Collection candidates, + PublicKey serverKey, List entries, + HostEntryPair[] modified) throws RevokedKeyException { for (HostEntryPair current : entries) { KnownHostEntry entry = current.getHostEntry(); for (SshdSocketAddress host : candidates) { @@ -355,33 +346,13 @@ public class OpenSshServerKeyVerifier return userFiles; } - private void updateKnownHostsFile(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey, Path path, - HostKeyHelper updater) - throws IOException { - KnownHostEntry entry = updater.prepareKnownHostEntry(clientSession, - remoteAddress, serverKey); - if (entry == null) { + private void updateKnownHostsFile(Collection candidates, + PublicKey serverKey, Path path, Configuration config) + throws Exception { + String newEntry = createHostKeyLine(candidates, serverKey, config); + if (newEntry == null) { return; } - if (!Files.exists(path)) { - if (askAboutNewFile) { - CredentialsProvider provider = getCredentialsProvider( - clientSession); - if (provider == null) { - // We can't ask, so don't create the file - return; - } - URIish uri = new URIish().setPath(path.toString()); - if (!askUser(provider, uri, // - format(SshdText.get().knownHostsUserAskCreationPrompt, - path), // - format(SshdText.get().knownHostsUserAskCreationMsg, - path))) { - return; - } - } - } LockFile lock = new LockFile(path.toFile()); if (lock.lockForAppend()) { try { @@ -389,7 +360,7 @@ public class OpenSshServerKeyVerifier new OutputStreamWriter(lock.getOutputStream(), UTF_8))) { writer.newLine(); - writer.write(entry.getConfigLine()); + writer.write(newEntry); writer.newLine(); } lock.commit(); @@ -403,15 +374,12 @@ public class OpenSshServerKeyVerifier } } - private void updateModifiedServerKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey, - HostEntryPair entry, Path path, HostKeyHelper helper) + private void updateModifiedServerKey(PublicKey serverKey, + HostEntryPair entry, Path path) throws IOException { KnownHostEntry hostEntry = entry.getHostEntry(); String oldLine = hostEntry.getConfigLine(); - String newLine = helper.prepareModifiedServerKeyLine(clientSession, - remoteAddress, hostEntry, oldLine, entry.getServerKey(), - serverKey); + String newLine = updateHostKeyLine(oldLine, serverKey); if (newLine == null || newLine.isEmpty()) { return; } @@ -454,78 +422,65 @@ public class OpenSshServerKeyVerifier } } - private static CredentialsProvider getCredentialsProvider( - ClientSession session) { - if (session instanceof JGitClientSession) { - return ((JGitClientSession) session).getCredentialsProvider(); - } - return null; - } + private static class AskUser { - private static boolean askUser(CredentialsProvider provider, URIish uri, - String prompt, String... messages) { - List items = new ArrayList<>(messages.length + 1); - for (String message : messages) { - items.add(new CredentialItem.InformationalMessage(message)); - } - if (prompt != null) { - CredentialItem.YesNoType answer = new CredentialItem.YesNoType( - prompt); - items.add(answer); - return provider.get(uri, items) && answer.getValue(); - } else { - return provider.get(uri, items); + public enum ModifiedKeyHandling { + DENY, ALLOW, ALLOW_AND_STORE } - } - - private static class AskUser { private enum Check { ASK, DENY, ALLOW; } - @SuppressWarnings("nls") - private Check checkMode(ClientSession session, - SocketAddress remoteAddress, boolean changed) { + private final @NonNull Configuration config; + + private final CredentialsProvider provider; + + public AskUser(@NonNull Configuration config, + CredentialsProvider provider) { + this.config = config; + this.provider = provider; + } + + private static boolean askUser(CredentialsProvider provider, URIish uri, + String prompt, String... messages) { + List items = new ArrayList<>(messages.length + 1); + for (String message : messages) { + items.add(new CredentialItem.InformationalMessage(message)); + } + if (prompt != null) { + CredentialItem.YesNoType answer = new CredentialItem.YesNoType( + prompt); + items.add(answer); + return provider.get(uri, items) && answer.getValue(); + } else { + return provider.get(uri, items); + } + } + + private Check checkMode(SocketAddress remoteAddress, boolean changed) { if (!(remoteAddress instanceof InetSocketAddress)) { return Check.DENY; } - if (session instanceof JGitClientSession) { - HostConfigEntry entry = ((JGitClientSession) session) - .getHostConfigEntry(); - String value = entry.getProperty( - SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); - switch (value.toLowerCase(Locale.ROOT)) { - case SshConstants.YES: - case SshConstants.ON: - return Check.DENY; - case SshConstants.NO: - case SshConstants.OFF: - return Check.ALLOW; - case "accept-new": - return changed ? Check.DENY : Check.ALLOW; - default: - break; - } - } - if (getCredentialsProvider(session) == null) { - // This is called only for new, unknown hosts. If we have no way - // to interact with the user, the fallback mode is to deny the - // key. + switch (config.getStrictHostKeyChecking()) { + case REQUIRE_MATCH: return Check.DENY; + case ACCEPT_ANY: + return Check.ALLOW; + case ACCEPT_NEW: + return changed ? Check.DENY : Check.ALLOW; + default: + return provider == null ? Check.DENY : Check.ASK; } - return Check.ASK; } - public void revokedKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey, Path path) { - CredentialsProvider provider = getCredentialsProvider( - clientSession); + public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey, + Path path) { if (provider == null) { return; } InetSocketAddress remote = (InetSocketAddress) remoteAddress; - URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), + URIish uri = JGitUserInteraction.toURI(config.getUsername(), remote); String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, serverKey); @@ -539,14 +494,12 @@ public class OpenSshServerKeyVerifier md5, sha256); } - public boolean acceptUnknownKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey) { - Check check = checkMode(clientSession, remoteAddress, false); + public boolean acceptUnknownKey(SocketAddress remoteAddress, + PublicKey serverKey) { + Check check = checkMode(remoteAddress, false); if (check != Check.ASK) { return check == Check.ALLOW; } - CredentialsProvider provider = getCredentialsProvider( - clientSession); InetSocketAddress remote = (InetSocketAddress) remoteAddress; // Ask the user String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, @@ -554,7 +507,7 @@ public class OpenSshServerKeyVerifier String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey); String keyAlgorithm = serverKey.getAlgorithm(); String remoteHost = remote.getHostString(); - URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), + URIish uri = JGitUserInteraction.toURI(config.getUsername(), remote); String prompt = SshdText.get().knownHostsUnknownKeyPrompt; return askUser(provider, uri, prompt, // @@ -566,19 +519,17 @@ public class OpenSshServerKeyVerifier } public ModifiedKeyHandling acceptModifiedServerKey( - ClientSession clientSession, - SocketAddress remoteAddress, PublicKey expected, + InetSocketAddress remoteAddress, PublicKey expected, PublicKey actual, Path path) { - Check check = checkMode(clientSession, remoteAddress, true); + Check check = checkMode(remoteAddress, true); if (check == Check.ALLOW) { // Never auto-store on CHECK.ALLOW return ModifiedKeyHandling.ALLOW; } - InetSocketAddress remote = (InetSocketAddress) remoteAddress; String keyAlgorithm = actual.getAlgorithm(); - String remoteHost = remote.getHostString(); - URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(), - remote); + String remoteHost = remoteAddress.getHostString(); + URIish uri = JGitUserInteraction.toURI(config.getUsername(), + remoteAddress); List messages = new ArrayList<>(); String warning = format( SshdText.get().knownHostsModifiedKeyWarning, @@ -589,8 +540,6 @@ public class OpenSshServerKeyVerifier KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual)); messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$ - CredentialsProvider provider = getCredentialsProvider( - clientSession); if (check == Check.DENY) { if (provider != null) { messages.add(format( @@ -618,6 +567,17 @@ public class OpenSshServerKeyVerifier return ModifiedKeyHandling.DENY; } + public boolean createNewFile(Path path) { + if (provider == null) { + // We can't ask, so don't create the file + return false; + } + URIish uri = new URIish().setPath(path.toString()); + return askUser(provider, uri, // + format(SshdText.get().knownHostsUserAskCreationPrompt, + path), // + format(SshdText.get().knownHostsUserAskCreationMsg, path)); + } } private static class HostKeyFile extends ModifiableFileWatcher @@ -694,50 +654,108 @@ public class OpenSshServerKeyVerifier } } - // The stuff below is just a hack to avoid having to copy a lot of code from - // KnownHostsServerKeyVerifier - - private static class HostKeyHelper extends KnownHostsServerKeyVerifier { - - public HostKeyHelper() { - // These two arguments will never be used in any way. - super((c, r, s) -> false, new File(".").toPath()); //$NON-NLS-1$ + private int parsePort(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return -1; } + } - @Override - protected KnownHostEntry prepareKnownHostEntry( - ClientSession clientSession, SocketAddress remoteAddress, - PublicKey serverKey) throws IOException { - // Make this method accessible - try { - return super.prepareKnownHostEntry(clientSession, remoteAddress, - serverKey); - } catch (Exception e) { - throw new IOException(e.getMessage(), e); + private SshdSocketAddress toSshdSocketAddress(@NonNull String address) { + String host = null; + int port = 0; + if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address + .charAt(0)) { + int end = address.indexOf( + HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM); + if (end <= 1) { + return null; // Invalid } + host = address.substring(1, end); + if (end < address.length() - 1 + && HostPatternsHolder.PORT_VALUE_DELIMITER == address + .charAt(end + 1)) { + port = parsePort(address.substring(end + 2)); + } + } else { + int i = address + .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER); + if (i > 0) { + port = parsePort(address.substring(i + 1)); + host = address.substring(0, i); + } else { + host = address; + } + } + if (port < 0 || port > 65535) { + return null; } + return new SshdSocketAddress(host, port); + } - @Override - protected String prepareModifiedServerKeyLine( - ClientSession clientSession, SocketAddress remoteAddress, - KnownHostEntry entry, String curLine, PublicKey expected, - PublicKey actual) throws IOException { - // Make this method accessible - try { - return super.prepareModifiedServerKeyLine(clientSession, - remoteAddress, entry, curLine, expected, actual); - } catch (Exception e) { - throw new IOException(e.getMessage(), e); + private Collection getCandidates( + @NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress) { + Collection candidates = new TreeSet<>( + SshdSocketAddress.BY_HOST_AND_PORT); + candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress)); + SshdSocketAddress address = toSshdSocketAddress(connectAddress); + if (address != null) { + candidates.add(address); + } + return candidates; + } + + private String createHostKeyLine(Collection patterns, + PublicKey key, Configuration config) throws Exception { + StringBuilder result = new StringBuilder(); + if (config.getHashKnownHosts()) { + // SHA1 is the only algorithm for host name hashing known to OpenSSH + // or to Apache MINA sshd. + NamedFactory digester = KnownHostDigest.SHA1; + Mac mac = digester.create(); + SecureRandom prng = new SecureRandom(); + byte[] salt = new byte[mac.getDefaultBlockSize()]; + for (SshdSocketAddress address : patterns) { + if (result.length() > 0) { + result.append(','); + } + prng.nextBytes(salt); + KnownHostHashValue.append(result, digester, salt, + KnownHostHashValue.calculateHashValue( + address.getHostName(), address.getPort(), mac, + salt)); + } + } else { + for (SshdSocketAddress address : patterns) { + if (result.length() > 0) { + result.append(','); + } + KnownHostHashValue.appendHostPattern(result, + address.getHostName(), address.getPort()); } } + result.append(' '); + PublicKeyEntry.appendPublicKeyEntry(result, key); + return result.toString(); + } - @Override - protected Collection resolveHostNetworkIdentities( - ClientSession clientSession, SocketAddress remoteAddress) { - // Make this method accessible - return super.resolveHostNetworkIdentities(clientSession, - remoteAddress); + private String updateHostKeyLine(String line, PublicKey newKey) + throws IOException { + // Replaces an existing public key by the new key + int pos = line.indexOf(' '); + if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) { + // We're at the end of the marker. Skip ahead to the next blank. + pos = line.indexOf(' ', pos + 1); + } + if (pos < 0) { + // Don't update if bogus format + return null; } + StringBuilder result = new StringBuilder(line.substring(0, pos + 1)); + PublicKeyEntry.appendPublicKeyEntry(result, newKey); + return result.toString(); } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java index 4f5f497f7..2baeb2887 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java @@ -43,9 +43,9 @@ package org.eclipse.jgit.internal.transport.sshd; import java.net.SocketAddress; +import java.security.PublicKey; import java.util.List; -import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; import org.apache.sshd.client.session.ClientSession; import org.eclipse.jgit.annotations.NonNull; @@ -55,7 +55,7 @@ import org.eclipse.jgit.annotations.NonNull; public interface ServerKeyLookup { /** - * Retrieves all entries for a given remote address. + * Retrieves all public keys known for a given remote. * * @param session * needed to determine the config files if specified in the ssh @@ -65,5 +65,5 @@ public interface ServerKeyLookup { * @return a possibly empty list of entries found, including revoked ones */ @NonNull - List lookup(ClientSession session, SocketAddress remote); + List lookup(ClientSession session, SocketAddress remote); } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java index 97e0da042..66e595c6c 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java @@ -62,8 +62,8 @@ public class DefaultProxyDataFactory implements ProxyDataFactory { public ProxyData get(InetSocketAddress remoteAddress) { try { List proxies = ProxySelector.getDefault() - .select(new URI(Proxy.Type.SOCKS.name(), - "//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$ + .select(new URI( + "socket://" + remoteAddress.getHostString())); //$NON-NLS-1$ ProxyData data = getData(proxies, Proxy.Type.SOCKS); if (data == null) { proxies = ProxySelector.getDefault() diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java new file mode 100644 index 000000000..bdfb96d0c --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * 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";