Merge branch 'stable-5.5'
* stable-5.5: Prepare 5.4.4-SNAPSHOT builds JGit v5.4.3.201909031940-r Prepare 5.3.6-SNAPSHOT builds JGit v5.3.5.201909031855-r Prepare 5.1.12-SNAPSHOT builds JGit v5.1.11.201909031202-r Prepare 4.11.10-SNAPSHOT builds JGit v4.11.9.201909030838-r Bazel: Update bazlets to the latest master revision Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file BatchRefUpdate: repro racy atomic update, and fix it Delete unused FileTreeIteratorWithTimeControl Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Silence API warnings sshd: fix proxy connections with the DefaultProxyDataFactory sshd: support the HashKnownHosts configuration sshd: configurable server key verification sshd: allow setting a null ssh config sshd: simplify OpenSshServerKeyVerifier sshd: simplify ServerKeyLookup interface Use https in update site URLs Change-Id: Icd21a8fcccffd56bfedbd037e48028308db6d13b Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
commit
4d78215673
|
@ -15,7 +15,7 @@ versions.check(minimum_bazel_version = "0.26.1")
|
||||||
|
|
||||||
load("//tools:bazlets.bzl", "load_bazlets")
|
load("//tools:bazlets.bzl", "load_bazlets")
|
||||||
|
|
||||||
load_bazlets(commit = "8528a0df69dadf6311d8d3f81c1b693afda8bcf1")
|
load_bazlets(commit = "09a035e98077dce549d5f6a7472d06c4b8f792d2")
|
||||||
|
|
||||||
load(
|
load(
|
||||||
"@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl",
|
"@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl",
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<component id="org.eclipse.jgit.lfs" version="2">
|
|
||||||
<resource path="src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java" type="org.eclipse.jgit.lfs.lib.AnyLongObjectId">
|
|
||||||
<filter id="1141899266">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.4"/>
|
|
||||||
<message_argument value="5.5"/>
|
|
||||||
<message_argument value="isEqual(AnyLongObjectId, AnyLongObjectId)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
</component>
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<plugin
|
<plugin
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<requires>
|
<requires>
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<requires>
|
<requires>
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<requires>
|
<requires>
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<includes
|
<includes
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<requires>
|
<requires>
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</license>
|
</license>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<update label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<update label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
<discovery label="%updateSiteName" url="http://download.eclipse.org/egit/updates"/>
|
<discovery label="%updateSiteName" url="https://download.eclipse.org/egit/updates"/>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<requires>
|
<requires>
|
||||||
|
|
|
@ -7,7 +7,15 @@ Bundle-Version: 5.6.0.qualifier
|
||||||
Bundle-Vendor: %Bundle-Vendor
|
Bundle-Vendor: %Bundle-Vendor
|
||||||
Bundle-Localization: plugin
|
Bundle-Localization: plugin
|
||||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
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.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;version="[5.6.0,5.7.0)",
|
||||||
org.eclipse.jgit.junit.ssh;version="[5.6.0,5.7.0)",
|
org.eclipse.jgit.junit.ssh;version="[5.6.0,5.7.0)",
|
||||||
|
|
|
@ -42,16 +42,22 @@
|
||||||
*/
|
*/
|
||||||
package org.eclipse.jgit.transport.sshd;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.Arrays;
|
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.api.errors.TransportException;
|
||||||
import org.eclipse.jgit.lib.Constants;
|
import org.eclipse.jgit.lib.Constants;
|
||||||
import org.eclipse.jgit.transport.SshSessionFactory;
|
import org.eclipse.jgit.transport.SshSessionFactory;
|
||||||
import org.eclipse.jgit.transport.ssh.SshTestBase;
|
import org.eclipse.jgit.transport.ssh.SshTestBase;
|
||||||
import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
|
|
||||||
import org.eclipse.jgit.util.FS;
|
import org.eclipse.jgit.util.FS;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.experimental.theories.Theories;
|
import org.junit.experimental.theories.Theories;
|
||||||
|
@ -101,6 +107,51 @@ public void testEd25519HostKey() throws Exception {
|
||||||
"IdentityFile " + privateKey1.getAbsolutePath());
|
"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<LogEntry> 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<String> 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
|
@Test
|
||||||
public void testPreamble() throws Exception {
|
public void testPreamble() throws Exception {
|
||||||
// Test that the client can deal with strange lines being sent before
|
// Test that the client can deal with strange lines being sent before
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
|
* 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<PublicKey> 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<KeyPair> 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<KeyPair> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<KeyPair> 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<KeyPair> 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<component id="org.eclipse.jgit.ssh.apache" version="2">
|
||||||
|
<resource path="src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java" type="org.eclipse.jgit.transport.sshd.ServerKeyDatabase">
|
||||||
|
<filter id="1108344834">
|
||||||
|
<message_arguments>
|
||||||
|
<message_argument value="5.5"/>
|
||||||
|
<message_argument value="5.6"/>
|
||||||
|
<message_argument value="org.eclipse.jgit.transport.sshd.ServerKeyDatabase"/>
|
||||||
|
</message_arguments>
|
||||||
|
</filter>
|
||||||
|
</resource>
|
||||||
|
<resource path="src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java" type="org.eclipse.jgit.transport.sshd.SshdSessionFactory">
|
||||||
|
<filter id="1141899266">
|
||||||
|
<message_arguments>
|
||||||
|
<message_argument value="5.5"/>
|
||||||
|
<message_argument value="5.6"/>
|
||||||
|
<message_argument value="getServerKeyDatabase(File, File)"/>
|
||||||
|
</message_arguments>
|
||||||
|
</filter>
|
||||||
|
<filter id="1141899266">
|
||||||
|
<message_arguments>
|
||||||
|
<message_argument value="5.5"/>
|
||||||
|
<message_argument value="5.6"/>
|
||||||
|
<message_argument value="getSshConfig(File)"/>
|
||||||
|
</message_arguments>
|
||||||
|
</filter>
|
||||||
|
</resource>
|
||||||
|
</component>
|
|
@ -57,7 +57,6 @@
|
||||||
|
|
||||||
import org.apache.sshd.client.ClientFactoryManager;
|
import org.apache.sshd.client.ClientFactoryManager;
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntry;
|
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.keyverifier.ServerKeyVerifier;
|
||||||
import org.apache.sshd.client.session.ClientSessionImpl;
|
import org.apache.sshd.client.session.ClientSessionImpl;
|
||||||
import org.apache.sshd.common.FactoryManager;
|
import org.apache.sshd.common.FactoryManager;
|
||||||
|
@ -293,11 +292,10 @@ protected String resolveAvailableSignaturesProposal(
|
||||||
if (verifier instanceof ServerKeyLookup) {
|
if (verifier instanceof ServerKeyLookup) {
|
||||||
SocketAddress remoteAddress = resolvePeerAddress(
|
SocketAddress remoteAddress = resolvePeerAddress(
|
||||||
resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS));
|
resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS));
|
||||||
List<HostEntryPair> allKnownKeys = ((ServerKeyLookup) verifier)
|
List<PublicKey> allKnownKeys = ((ServerKeyLookup) verifier)
|
||||||
.lookup(this, remoteAddress);
|
.lookup(this, remoteAddress);
|
||||||
Set<String> reordered = new LinkedHashSet<>();
|
Set<String> reordered = new LinkedHashSet<>();
|
||||||
for (HostEntryPair h : allKnownKeys) {
|
for (PublicKey key : allKnownKeys) {
|
||||||
PublicKey key = h.getServerKey();
|
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
String keyType = KeyUtils.getKeyType(key);
|
String keyType = KeyUtils.getKeyType(key);
|
||||||
if (keyType != null) {
|
if (keyType != null) {
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
|
* 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<PublicKey> 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<String> 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<String> getUserKnownHostsFiles() {
|
||||||
|
return get(SshConstants.USER_KNOWN_HOSTS_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -177,6 +177,10 @@ private InetSocketAddress configureProxy(ProxyData proxyData,
|
||||||
return remoteAddress;
|
return remoteAddress;
|
||||||
}
|
}
|
||||||
InetSocketAddress address = (InetSocketAddress) proxy.address();
|
InetSocketAddress address = (InetSocketAddress) proxy.address();
|
||||||
|
if (address.isUnresolved()) {
|
||||||
|
address = new InetSocketAddress(address.getHostName(),
|
||||||
|
address.getPort());
|
||||||
|
}
|
||||||
switch (proxy.type()) {
|
switch (proxy.type()) {
|
||||||
case HTTP:
|
case HTTP:
|
||||||
setClientProxyConnector(
|
setClientProxyConnector(
|
||||||
|
|
|
@ -83,7 +83,9 @@
|
||||||
*/
|
*/
|
||||||
public class JGitSshConfig implements HostConfigEntryResolver {
|
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
|
* Creates a new {@link OpenSshConfigFile} that will read the config from
|
||||||
|
@ -92,20 +94,22 @@ public class JGitSshConfig implements HostConfigEntryResolver {
|
||||||
* @param home
|
* @param home
|
||||||
* user's home directory for the purpose of ~ replacement
|
* user's home directory for the purpose of ~ replacement
|
||||||
* @param config
|
* @param config
|
||||||
* file to load.
|
* file to load; may be {@code null} if no ssh config file
|
||||||
|
* handling is desired
|
||||||
* @param localUserName
|
* @param localUserName
|
||||||
* user name of the current user on the local host OS
|
* 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) {
|
@NonNull String localUserName) {
|
||||||
configFile = new OpenSshConfigFile(home, config, localUserName);
|
this.localUserName = localUserName;
|
||||||
|
configFile = config == null ? null : new OpenSshConfigFile(home, config, localUserName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HostConfigEntry resolveEffectiveHost(String host, int port,
|
public HostConfigEntry resolveEffectiveHost(String host, int port,
|
||||||
SocketAddress localAddress, String username,
|
SocketAddress localAddress, String username,
|
||||||
AttributeRepository attributes) throws IOException {
|
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();
|
JGitHostConfigEntry config = new JGitHostConfigEntry();
|
||||||
// Apache MINA conflates all keys, even multi-valued ones, in one map
|
// Apache MINA conflates all keys, even multi-valued ones, in one map
|
||||||
// and puts multiple values separated by commas in one string. See
|
// and puts multiple values separated by commas in one string. See
|
||||||
|
@ -131,7 +135,7 @@ public HostConfigEntry resolveEffectiveHost(String host, int port,
|
||||||
String user = username != null && !username.isEmpty() ? username
|
String user = username != null && !username.isEmpty() ? username
|
||||||
: entry.getValue(SshConstants.USER);
|
: entry.getValue(SshConstants.USER);
|
||||||
if (user == null || user.isEmpty()) {
|
if (user == null || user.isEmpty()) {
|
||||||
user = configFile.getLocalUserName();
|
user = localUserName;
|
||||||
}
|
}
|
||||||
config.setUsername(user);
|
config.setUsername(user);
|
||||||
config.setProperty(SshConstants.USER, user);
|
config.setProperty(SshConstants.USER, user);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
|
* Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
* and other copyright owners as documented in the project's IP log.
|
* and other copyright owners as documented in the project's IP log.
|
||||||
*
|
*
|
||||||
* This program and the accompanying materials are made available
|
* This program and the accompanying materials are made available
|
||||||
|
@ -47,7 +47,6 @@
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
@ -59,34 +58,39 @@
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Supplier;
|
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.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.KnownHostsServerKeyVerifier.HostEntryPair;
|
||||||
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
|
||||||
import org.apache.sshd.client.session.ClientSession;
|
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.AuthorizedKeyEntry;
|
||||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
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.config.keys.PublicKeyEntryResolver;
|
||||||
import org.apache.sshd.common.digest.BuiltinDigests;
|
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.io.ModifiableFileWatcher;
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress;
|
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.internal.storage.file.LockFile;
|
||||||
import org.eclipse.jgit.transport.CredentialItem;
|
import org.eclipse.jgit.transport.CredentialItem;
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||||
import org.eclipse.jgit.transport.SshConstants;
|
|
||||||
import org.eclipse.jgit.transport.URIish;
|
import org.eclipse.jgit.transport.URIish;
|
||||||
|
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -148,14 +152,14 @@
|
||||||
* @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
|
* @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
|
||||||
* ssh-config</a>
|
* ssh-config</a>
|
||||||
*/
|
*/
|
||||||
public class OpenSshServerKeyVerifier
|
public class OpenSshServerKeyDatabase
|
||||||
implements ServerKeyVerifier, ServerKeyLookup {
|
implements ServerKeyDatabase {
|
||||||
|
|
||||||
// TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
|
// TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
|
||||||
// files may be large!
|
// files may be large!
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory
|
private static final Logger LOG = LoggerFactory
|
||||||
.getLogger(OpenSshServerKeyVerifier.class);
|
.getLogger(OpenSshServerKeyDatabase.class);
|
||||||
|
|
||||||
/** Can be used to mark revoked known host lines. */
|
/** Can be used to mark revoked known host lines. */
|
||||||
private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
|
private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
|
||||||
|
@ -166,12 +170,8 @@ public class OpenSshServerKeyVerifier
|
||||||
|
|
||||||
private final List<HostKeyFile> defaultFiles = new ArrayList<>();
|
private final List<HostKeyFile> defaultFiles = new ArrayList<>();
|
||||||
|
|
||||||
private enum ModifiedKeyHandling {
|
|
||||||
DENY, ALLOW, ALLOW_AND_STORE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link OpenSshServerKeyVerifier}.
|
* Creates a new {@link OpenSshServerKeyDatabase}.
|
||||||
*
|
*
|
||||||
* @param askAboutNewFile
|
* @param askAboutNewFile
|
||||||
* whether to ask the user, if possible, about creating a new
|
* whether to ask the user, if possible, about creating a new
|
||||||
|
@ -181,7 +181,7 @@ private enum ModifiedKeyHandling {
|
||||||
* empty or {@code null}, in which case no default files are
|
* empty or {@code null}, in which case no default files are
|
||||||
* installed. The files need not exist.
|
* installed. The files need not exist.
|
||||||
*/
|
*/
|
||||||
public OpenSshServerKeyVerifier(boolean askAboutNewFile,
|
public OpenSshServerKeyDatabase(boolean askAboutNewFile,
|
||||||
List<Path> defaultFiles) {
|
List<Path> defaultFiles) {
|
||||||
if (defaultFiles != null) {
|
if (defaultFiles != null) {
|
||||||
for (Path file : defaultFiles) {
|
for (Path file : defaultFiles) {
|
||||||
|
@ -193,38 +193,30 @@ public OpenSshServerKeyVerifier(boolean askAboutNewFile,
|
||||||
this.askAboutNewFile = askAboutNewFile;
|
this.askAboutNewFile = askAboutNewFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HostKeyFile> getFilesToUse(ClientSession session) {
|
private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
|
||||||
List<HostKeyFile> filesToUse = defaultFiles;
|
List<HostKeyFile> filesToUse = defaultFiles;
|
||||||
if (session instanceof JGitClientSession) {
|
List<HostKeyFile> userFiles = addUserHostKeyFiles(
|
||||||
HostConfigEntry entry = ((JGitClientSession) session)
|
config.getUserKnownHostsFiles());
|
||||||
.getHostConfigEntry();
|
if (!userFiles.isEmpty()) {
|
||||||
if (entry instanceof JGitHostConfigEntry) {
|
filesToUse = userFiles;
|
||||||
// Always true!
|
|
||||||
List<HostKeyFile> userFiles = addUserHostKeyFiles(
|
|
||||||
((JGitHostConfigEntry) entry).getMultiValuedOptions()
|
|
||||||
.get(SshConstants.USER_KNOWN_HOSTS_FILE));
|
|
||||||
if (!userFiles.isEmpty()) {
|
|
||||||
filesToUse = userFiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return filesToUse;
|
return filesToUse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<HostEntryPair> lookup(ClientSession session,
|
public List<PublicKey> lookup(@NonNull String connectAddress,
|
||||||
SocketAddress remote) {
|
@NonNull InetSocketAddress remoteAddress,
|
||||||
List<HostKeyFile> filesToUse = getFilesToUse(session);
|
@NonNull Configuration config) {
|
||||||
HostKeyHelper helper = new HostKeyHelper();
|
List<HostKeyFile> filesToUse = getFilesToUse(config);
|
||||||
List<HostEntryPair> result = new ArrayList<>();
|
List<PublicKey> result = new ArrayList<>();
|
||||||
Collection<SshdSocketAddress> candidates = helper
|
Collection<SshdSocketAddress> candidates = getCandidates(
|
||||||
.resolveHostNetworkIdentities(session, remote);
|
connectAddress, remoteAddress);
|
||||||
for (HostKeyFile file : filesToUse) {
|
for (HostKeyFile file : filesToUse) {
|
||||||
for (HostEntryPair current : file.get()) {
|
for (HostEntryPair current : file.get()) {
|
||||||
KnownHostEntry entry = current.getHostEntry();
|
KnownHostEntry entry = current.getHostEntry();
|
||||||
for (SshdSocketAddress host : candidates) {
|
for (SshdSocketAddress host : candidates) {
|
||||||
if (entry.isHostMatch(host.getHostName(), host.getPort())) {
|
if (entry.isHostMatch(host.getHostName(), host.getPort())) {
|
||||||
result.add(current);
|
result.add(current.getServerKey());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,22 +226,23 @@ public List<HostEntryPair> lookup(ClientSession session,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean verifyServerKey(ClientSession clientSession,
|
public boolean accept(@NonNull String connectAddress,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey) {
|
@NonNull InetSocketAddress remoteAddress,
|
||||||
List<HostKeyFile> filesToUse = getFilesToUse(clientSession);
|
@NonNull PublicKey serverKey,
|
||||||
AskUser ask = new AskUser();
|
@NonNull Configuration config, CredentialsProvider provider) {
|
||||||
|
List<HostKeyFile> filesToUse = getFilesToUse(config);
|
||||||
|
AskUser ask = new AskUser(config, provider);
|
||||||
HostEntryPair[] modified = { null };
|
HostEntryPair[] modified = { null };
|
||||||
Path path = null;
|
Path path = null;
|
||||||
HostKeyHelper helper = new HostKeyHelper();
|
Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
|
||||||
|
remoteAddress);
|
||||||
for (HostKeyFile file : filesToUse) {
|
for (HostKeyFile file : filesToUse) {
|
||||||
try {
|
try {
|
||||||
if (find(clientSession, remoteAddress, serverKey, file.get(),
|
if (find(candidates, serverKey, file.get(), modified)) {
|
||||||
modified, helper)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (RevokedKeyException e) {
|
} catch (RevokedKeyException e) {
|
||||||
ask.revokedKey(clientSession, remoteAddress, serverKey,
|
ask.revokedKey(remoteAddress, serverKey, file.getPath());
|
||||||
file.getPath());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (path == null && modified[0] != null) {
|
if (path == null && modified[0] != null) {
|
||||||
|
@ -260,20 +253,19 @@ public boolean verifyServerKey(ClientSession clientSession,
|
||||||
}
|
}
|
||||||
if (modified[0] != null) {
|
if (modified[0] != null) {
|
||||||
// We found an entry, but with a different key
|
// We found an entry, but with a different key
|
||||||
ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
|
AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
|
||||||
clientSession, remoteAddress, modified[0].getServerKey(),
|
remoteAddress, modified[0].getServerKey(),
|
||||||
serverKey, path);
|
serverKey, path);
|
||||||
if (toDo == ModifiedKeyHandling.ALLOW_AND_STORE) {
|
if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
|
||||||
try {
|
try {
|
||||||
updateModifiedServerKey(clientSession, remoteAddress,
|
updateModifiedServerKey(serverKey, modified[0], path);
|
||||||
serverKey, modified[0], path, helper);
|
|
||||||
knownHostsFiles.get(path).resetReloadAttributes();
|
knownHostsFiles.get(path).resetReloadAttributes();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
|
LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
|
||||||
path));
|
path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toDo == ModifiedKeyHandling.DENY) {
|
if (toDo == AskUser.ModifiedKeyHandling.DENY) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// TODO: OpenSsh disables password and keyboard-interactive
|
// TODO: OpenSsh disables password and keyboard-interactive
|
||||||
|
@ -281,18 +273,20 @@ public boolean verifyServerKey(ClientSession clientSession,
|
||||||
// are switched off. (Plus a few other things such as X11 forwarding
|
// are switched off. (Plus a few other things such as X11 forwarding
|
||||||
// that are of no interest to a git client.)
|
// that are of no interest to a git client.)
|
||||||
return true;
|
return true;
|
||||||
} else if (ask.acceptUnknownKey(clientSession, remoteAddress,
|
} else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
|
||||||
serverKey)) {
|
|
||||||
if (!filesToUse.isEmpty()) {
|
if (!filesToUse.isEmpty()) {
|
||||||
HostKeyFile toUpdate = filesToUse.get(0);
|
HostKeyFile toUpdate = filesToUse.get(0);
|
||||||
path = toUpdate.getPath();
|
path = toUpdate.getPath();
|
||||||
try {
|
try {
|
||||||
updateKnownHostsFile(clientSession, remoteAddress,
|
if (Files.exists(path) || !askAboutNewFile
|
||||||
serverKey, path, helper);
|
|| ask.createNewFile(path)) {
|
||||||
toUpdate.resetReloadAttributes();
|
updateKnownHostsFile(candidates, serverKey, path,
|
||||||
} catch (IOException e) {
|
config);
|
||||||
|
toUpdate.resetReloadAttributes();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
|
LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
|
||||||
path));
|
path), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -304,12 +298,9 @@ private static class RevokedKeyException extends Exception {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean find(ClientSession clientSession,
|
private boolean find(Collection<SshdSocketAddress> candidates,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey,
|
PublicKey serverKey, List<HostEntryPair> entries,
|
||||||
List<HostEntryPair> entries, HostEntryPair[] modified,
|
HostEntryPair[] modified) throws RevokedKeyException {
|
||||||
HostKeyHelper helper) throws RevokedKeyException {
|
|
||||||
Collection<SshdSocketAddress> candidates = helper
|
|
||||||
.resolveHostNetworkIdentities(clientSession, remoteAddress);
|
|
||||||
for (HostEntryPair current : entries) {
|
for (HostEntryPair current : entries) {
|
||||||
KnownHostEntry entry = current.getHostEntry();
|
KnownHostEntry entry = current.getHostEntry();
|
||||||
for (SshdSocketAddress host : candidates) {
|
for (SshdSocketAddress host : candidates) {
|
||||||
|
@ -355,33 +346,13 @@ private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
|
||||||
return userFiles;
|
return userFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateKnownHostsFile(ClientSession clientSession,
|
private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey, Path path,
|
PublicKey serverKey, Path path, Configuration config)
|
||||||
HostKeyHelper updater)
|
throws Exception {
|
||||||
throws IOException {
|
String newEntry = createHostKeyLine(candidates, serverKey, config);
|
||||||
KnownHostEntry entry = updater.prepareKnownHostEntry(clientSession,
|
if (newEntry == null) {
|
||||||
remoteAddress, serverKey);
|
|
||||||
if (entry == null) {
|
|
||||||
return;
|
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());
|
LockFile lock = new LockFile(path.toFile());
|
||||||
if (lock.lockForAppend()) {
|
if (lock.lockForAppend()) {
|
||||||
try {
|
try {
|
||||||
|
@ -389,7 +360,7 @@ private void updateKnownHostsFile(ClientSession clientSession,
|
||||||
new OutputStreamWriter(lock.getOutputStream(),
|
new OutputStreamWriter(lock.getOutputStream(),
|
||||||
UTF_8))) {
|
UTF_8))) {
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
writer.write(entry.getConfigLine());
|
writer.write(newEntry);
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
}
|
}
|
||||||
lock.commit();
|
lock.commit();
|
||||||
|
@ -403,15 +374,12 @@ private void updateKnownHostsFile(ClientSession clientSession,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateModifiedServerKey(ClientSession clientSession,
|
private void updateModifiedServerKey(PublicKey serverKey,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey,
|
HostEntryPair entry, Path path)
|
||||||
HostEntryPair entry, Path path, HostKeyHelper helper)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
KnownHostEntry hostEntry = entry.getHostEntry();
|
KnownHostEntry hostEntry = entry.getHostEntry();
|
||||||
String oldLine = hostEntry.getConfigLine();
|
String oldLine = hostEntry.getConfigLine();
|
||||||
String newLine = helper.prepareModifiedServerKeyLine(clientSession,
|
String newLine = updateHostKeyLine(oldLine, serverKey);
|
||||||
remoteAddress, hostEntry, oldLine, entry.getServerKey(),
|
|
||||||
serverKey);
|
|
||||||
if (newLine == null || newLine.isEmpty()) {
|
if (newLine == null || newLine.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -454,78 +422,65 @@ private void updateModifiedServerKey(ClientSession clientSession,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CredentialsProvider getCredentialsProvider(
|
|
||||||
ClientSession session) {
|
|
||||||
if (session instanceof JGitClientSession) {
|
|
||||||
return ((JGitClientSession) session).getCredentialsProvider();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean askUser(CredentialsProvider provider, URIish uri,
|
|
||||||
String prompt, String... messages) {
|
|
||||||
List<CredentialItem> 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 static class AskUser {
|
private static class AskUser {
|
||||||
|
|
||||||
|
public enum ModifiedKeyHandling {
|
||||||
|
DENY, ALLOW, ALLOW_AND_STORE
|
||||||
|
}
|
||||||
|
|
||||||
private enum Check {
|
private enum Check {
|
||||||
ASK, DENY, ALLOW;
|
ASK, DENY, ALLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("nls")
|
private final @NonNull Configuration config;
|
||||||
private Check checkMode(ClientSession session,
|
|
||||||
SocketAddress remoteAddress, boolean changed) {
|
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<CredentialItem> 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)) {
|
if (!(remoteAddress instanceof InetSocketAddress)) {
|
||||||
return Check.DENY;
|
return Check.DENY;
|
||||||
}
|
}
|
||||||
if (session instanceof JGitClientSession) {
|
switch (config.getStrictHostKeyChecking()) {
|
||||||
HostConfigEntry entry = ((JGitClientSession) session)
|
case REQUIRE_MATCH:
|
||||||
.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.
|
|
||||||
return Check.DENY;
|
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,
|
public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey, Path path) {
|
Path path) {
|
||||||
CredentialsProvider provider = getCredentialsProvider(
|
|
||||||
clientSession);
|
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
|
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
|
||||||
URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
|
URIish uri = JGitUserInteraction.toURI(config.getUsername(),
|
||||||
remote);
|
remote);
|
||||||
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
|
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
|
||||||
serverKey);
|
serverKey);
|
||||||
|
@ -539,14 +494,12 @@ public void revokedKey(ClientSession clientSession,
|
||||||
md5, sha256);
|
md5, sha256);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean acceptUnknownKey(ClientSession clientSession,
|
public boolean acceptUnknownKey(SocketAddress remoteAddress,
|
||||||
SocketAddress remoteAddress, PublicKey serverKey) {
|
PublicKey serverKey) {
|
||||||
Check check = checkMode(clientSession, remoteAddress, false);
|
Check check = checkMode(remoteAddress, false);
|
||||||
if (check != Check.ASK) {
|
if (check != Check.ASK) {
|
||||||
return check == Check.ALLOW;
|
return check == Check.ALLOW;
|
||||||
}
|
}
|
||||||
CredentialsProvider provider = getCredentialsProvider(
|
|
||||||
clientSession);
|
|
||||||
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
|
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
|
||||||
// Ask the user
|
// Ask the user
|
||||||
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
|
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
|
||||||
|
@ -554,7 +507,7 @@ public boolean acceptUnknownKey(ClientSession clientSession,
|
||||||
String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
|
String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
|
||||||
String keyAlgorithm = serverKey.getAlgorithm();
|
String keyAlgorithm = serverKey.getAlgorithm();
|
||||||
String remoteHost = remote.getHostString();
|
String remoteHost = remote.getHostString();
|
||||||
URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
|
URIish uri = JGitUserInteraction.toURI(config.getUsername(),
|
||||||
remote);
|
remote);
|
||||||
String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
|
String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
|
||||||
return askUser(provider, uri, prompt, //
|
return askUser(provider, uri, prompt, //
|
||||||
|
@ -566,19 +519,17 @@ public boolean acceptUnknownKey(ClientSession clientSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModifiedKeyHandling acceptModifiedServerKey(
|
public ModifiedKeyHandling acceptModifiedServerKey(
|
||||||
ClientSession clientSession,
|
InetSocketAddress remoteAddress, PublicKey expected,
|
||||||
SocketAddress remoteAddress, PublicKey expected,
|
|
||||||
PublicKey actual, Path path) {
|
PublicKey actual, Path path) {
|
||||||
Check check = checkMode(clientSession, remoteAddress, true);
|
Check check = checkMode(remoteAddress, true);
|
||||||
if (check == Check.ALLOW) {
|
if (check == Check.ALLOW) {
|
||||||
// Never auto-store on CHECK.ALLOW
|
// Never auto-store on CHECK.ALLOW
|
||||||
return ModifiedKeyHandling.ALLOW;
|
return ModifiedKeyHandling.ALLOW;
|
||||||
}
|
}
|
||||||
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
|
|
||||||
String keyAlgorithm = actual.getAlgorithm();
|
String keyAlgorithm = actual.getAlgorithm();
|
||||||
String remoteHost = remote.getHostString();
|
String remoteHost = remoteAddress.getHostString();
|
||||||
URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
|
URIish uri = JGitUserInteraction.toURI(config.getUsername(),
|
||||||
remote);
|
remoteAddress);
|
||||||
List<String> messages = new ArrayList<>();
|
List<String> messages = new ArrayList<>();
|
||||||
String warning = format(
|
String warning = format(
|
||||||
SshdText.get().knownHostsModifiedKeyWarning,
|
SshdText.get().knownHostsModifiedKeyWarning,
|
||||||
|
@ -589,8 +540,6 @@ public ModifiedKeyHandling acceptModifiedServerKey(
|
||||||
KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
|
KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
|
||||||
messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
|
messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
|
||||||
|
|
||||||
CredentialsProvider provider = getCredentialsProvider(
|
|
||||||
clientSession);
|
|
||||||
if (check == Check.DENY) {
|
if (check == Check.DENY) {
|
||||||
if (provider != null) {
|
if (provider != null) {
|
||||||
messages.add(format(
|
messages.add(format(
|
||||||
|
@ -618,6 +567,17 @@ public ModifiedKeyHandling acceptModifiedServerKey(
|
||||||
return ModifiedKeyHandling.DENY;
|
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
|
private static class HostKeyFile extends ModifiableFileWatcher
|
||||||
|
@ -694,50 +654,108 @@ private List<HostEntryPair> reload(Path path) throws IOException {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stuff below is just a hack to avoid having to copy a lot of code from
|
private int parsePort(String s) {
|
||||||
// KnownHostsServerKeyVerifier
|
try {
|
||||||
|
return Integer.parseInt(s);
|
||||||
private static class HostKeyHelper extends KnownHostsServerKeyVerifier {
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
public HostKeyHelper() {
|
|
||||||
// These two arguments will never be used in any way.
|
|
||||||
super((c, r, s) -> false, new File(".").toPath()); //$NON-NLS-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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Collection<SshdSocketAddress> resolveHostNetworkIdentities(
|
|
||||||
ClientSession clientSession, SocketAddress remoteAddress) {
|
|
||||||
// Make this method accessible
|
|
||||||
return super.resolveHostNetworkIdentities(clientSession,
|
|
||||||
remoteAddress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<SshdSocketAddress> getCandidates(
|
||||||
|
@NonNull String connectAddress,
|
||||||
|
@NonNull InetSocketAddress remoteAddress) {
|
||||||
|
Collection<SshdSocketAddress> 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<SshdSocketAddress> 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<Mac> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -43,9 +43,9 @@
|
||||||
package org.eclipse.jgit.internal.transport.sshd;
|
package org.eclipse.jgit.internal.transport.sshd;
|
||||||
|
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
|
|
||||||
import org.apache.sshd.client.session.ClientSession;
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
import org.eclipse.jgit.annotations.NonNull;
|
import org.eclipse.jgit.annotations.NonNull;
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
public interface ServerKeyLookup {
|
public interface ServerKeyLookup {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all entries for a given remote address.
|
* Retrieves all public keys known for a given remote.
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* needed to determine the config files if specified in the ssh
|
* 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
|
* @return a possibly empty list of entries found, including revoked ones
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
List<HostEntryPair> lookup(ClientSession session, SocketAddress remote);
|
List<PublicKey> lookup(ClientSession session, SocketAddress remote);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,8 +62,8 @@ public class DefaultProxyDataFactory implements ProxyDataFactory {
|
||||||
public ProxyData get(InetSocketAddress remoteAddress) {
|
public ProxyData get(InetSocketAddress remoteAddress) {
|
||||||
try {
|
try {
|
||||||
List<Proxy> proxies = ProxySelector.getDefault()
|
List<Proxy> proxies = ProxySelector.getDefault()
|
||||||
.select(new URI(Proxy.Type.SOCKS.name(),
|
.select(new URI(
|
||||||
"//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$
|
"socket://" + remoteAddress.getHostString())); //$NON-NLS-1$
|
||||||
ProxyData data = getData(proxies, Proxy.Type.SOCKS);
|
ProxyData data = getData(proxies, Proxy.Type.SOCKS);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
proxies = ProxySelector.getDefault()
|
proxies = ProxySelector.getDefault()
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<PublicKey> 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<String> getUserKnownHostsFiles();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of file names from the "GlobalKnownHostsFile" ssh
|
||||||
|
* config.
|
||||||
|
*
|
||||||
|
* @return the list as configured, with ~ already replaced
|
||||||
|
*/
|
||||||
|
List<String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
|
* Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
* and other copyright owners as documented in the project's IP log.
|
* and other copyright owners as documented in the project's IP log.
|
||||||
*
|
*
|
||||||
* This program and the accompanying materials are made available
|
* This program and the accompanying materials are made available
|
||||||
|
@ -66,7 +66,6 @@
|
||||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||||
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
|
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
|
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.NamedFactory;
|
||||||
import org.apache.sshd.common.compression.BuiltinCompressions;
|
import org.apache.sshd.common.compression.BuiltinCompressions;
|
||||||
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||||
|
@ -77,10 +76,11 @@
|
||||||
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
|
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction;
|
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.PasswordProviderWrapper;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.SshdText;
|
import org.eclipse.jgit.internal.transport.sshd.SshdText;
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||||
|
@ -104,7 +104,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
|
||||||
|
|
||||||
private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
|
private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<Tuple, ServerKeyVerifier> defaultServerKeyVerifier = new ConcurrentHashMap<>();
|
private final Map<Tuple, ServerKeyDatabase> defaultServerKeyDatabase = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
|
private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ -226,7 +226,8 @@ public SshdSession getSession(URIish uri,
|
||||||
.filePasswordProvider(
|
.filePasswordProvider(
|
||||||
createFilePasswordProvider(passphrases))
|
createFilePasswordProvider(passphrases))
|
||||||
.hostConfigEntryResolver(configFile)
|
.hostConfigEntryResolver(configFile)
|
||||||
.serverKeyVerifier(getServerKeyVerifier(home, sshDir))
|
.serverKeyVerifier(new JGitServerKeyVerifier(
|
||||||
|
getServerKeyDatabase(home, sshDir)))
|
||||||
.compressionFactories(
|
.compressionFactories(
|
||||||
new ArrayList<>(BuiltinCompressions.VALUES))
|
new ArrayList<>(BuiltinCompressions.VALUES))
|
||||||
.build();
|
.build();
|
||||||
|
@ -360,34 +361,48 @@ private HostConfigEntryResolver getHostConfigEntryResolver(
|
||||||
@NonNull File homeDir, @NonNull File sshDir) {
|
@NonNull File homeDir, @NonNull File sshDir) {
|
||||||
return defaultHostConfigEntryResolver.computeIfAbsent(
|
return defaultHostConfigEntryResolver.computeIfAbsent(
|
||||||
new Tuple(new Object[] { homeDir, sshDir }),
|
new Tuple(new Object[] { homeDir, sshDir }),
|
||||||
t -> new JGitSshConfig(homeDir,
|
t -> new JGitSshConfig(homeDir, getSshConfig(sshDir),
|
||||||
new File(sshDir, SshConstants.CONFIG),
|
|
||||||
getLocalUserName()));
|
getLocalUserName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain a {@link ServerKeyVerifier} to read known_hosts files and to
|
* Determines the ssh config file. The default implementation returns
|
||||||
* verify server host keys. The default implementation returns a
|
* ~/.ssh/config. If the file does not exist and is created later it will be
|
||||||
* {@link ServerKeyVerifier} that recognizes the two openssh standard files
|
* picked up. To not use a config file at all, return {@code null}.
|
||||||
* {@code ~/.ssh/known_hosts} and {@code ~/.ssh/known_hosts2} as well as any
|
*
|
||||||
* files configured via the {@code UserKnownHostsFile} option in the ssh
|
* @param sshDir
|
||||||
* config file.
|
* 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
|
* @param homeDir
|
||||||
* home directory to use for ~ replacement
|
* home directory to use for ~ replacement
|
||||||
* @param sshDir
|
* @param sshDir
|
||||||
* representing ~/.ssh/
|
* representing ~/.ssh/
|
||||||
* @return the resolver
|
* @return the {@link ServerKeyDatabase}
|
||||||
|
* @since 5.5
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir,
|
protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
|
||||||
@NonNull File sshDir) {
|
@NonNull File sshDir) {
|
||||||
return defaultServerKeyVerifier.computeIfAbsent(
|
return defaultServerKeyDatabase.computeIfAbsent(
|
||||||
new Tuple(new Object[] { homeDir, sshDir }),
|
new Tuple(new Object[] { homeDir, sshDir }),
|
||||||
t -> new OpenSshServerKeyVerifier(true,
|
t -> new OpenSshServerKeyDatabase(true,
|
||||||
getDefaultKnownHostsFiles(sshDir)));
|
getDefaultKnownHostsFiles(sshDir)));
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Gets the list of default user known hosts files. The default returns
|
* Gets the list of default user known hosts files. The default returns
|
||||||
* ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
|
* ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
|
||||||
|
@ -540,7 +555,7 @@ private List<NamedFactory<UserAuth>> getUserAuthFactories() {
|
||||||
* the ssh config defines {@code PreferredAuthentications} the value from
|
* the ssh config defines {@code PreferredAuthentications} the value from
|
||||||
* the ssh config takes precedence.
|
* 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
|
* none
|
||||||
*/
|
*/
|
||||||
protected String getDefaultPreferredAuthentications() {
|
protected String getDefaultPreferredAuthentications() {
|
||||||
|
|
|
@ -305,7 +305,7 @@ public void testSshWithoutKnownHostsWithProviderAsk()
|
||||||
// without provider. If it works, the server host key was written
|
// without provider. If it works, the server host key was written
|
||||||
// correctly.
|
// correctly.
|
||||||
File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
|
File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
|
||||||
cloneWith("ssh://localhost/doesntmatter", clonedAgain, provider, //
|
cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
|
||||||
"Host localhost", //
|
"Host localhost", //
|
||||||
"HostName localhost", //
|
"HostName localhost", //
|
||||||
"Port " + testPort, //
|
"Port " + testPort, //
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
|
|
||||||
package org.eclipse.jgit.internal.storage.file;
|
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.NANOSECONDS;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.LOCK_FAILURE;
|
import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.LOCK_FAILURE;
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -161,6 +163,33 @@ public void removeListener() {
|
||||||
refsChangedEvents = 0;
|
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
|
@Test
|
||||||
public void simpleNoForce() throws IOException {
|
public void simpleNoForce() throws IOException {
|
||||||
writeLooseRef("refs/heads/master", A);
|
writeLooseRef("refs/heads/master", A);
|
||||||
|
|
|
@ -1,166 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<component id="org.eclipse.jgit" version="2">
|
<component id="org.eclipse.jgit" version="2">
|
||||||
<resource path="src/org/eclipse/jgit/dircache/DirCacheEntry.java" type="org.eclipse.jgit.dircache.DirCacheEntry">
|
<resource path="src/org/eclipse/jgit/transport/SshConstants.java" type="org.eclipse.jgit.transport.SshConstants">
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getLastModifiedInstant()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="mightBeRacilyClean(Instant)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="setLastModified(Instant)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/lib/AnyObjectId.java" type="org.eclipse.jgit.lib.AnyObjectId">
|
|
||||||
<filter id="1141899266">
|
<filter id="1141899266">
|
||||||
<message_arguments>
|
<message_arguments>
|
||||||
<message_argument value="5.4"/>
|
|
||||||
<message_argument value="5.5"/>
|
<message_argument value="5.5"/>
|
||||||
<message_argument value="isEqual(AnyObjectId, AnyObjectId)"/>
|
<message_argument value="5.6"/>
|
||||||
</message_arguments>
|
<message_argument value="HASH_KNOWN_HOSTS"/>
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="CONFIG_FILESYSTEM_SECTION"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="CONFIG_KEY_MIN_RACY_THRESHOLD"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="CONFIG_KEY_TIMESTAMP_RESOLUTION"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator">
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getEntryLastModifiedInstant()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java" type="org.eclipse.jgit.treewalk.WorkingTreeIterator$Entry">
|
|
||||||
<filter id="336695337">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry"/>
|
|
||||||
<message_argument value="getLastModifiedInstant()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getLastModifiedInstant()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS">
|
|
||||||
<filter id="338792546">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="org.eclipse.jgit.util.FS"/>
|
|
||||||
<message_argument value="getFsTimerResolution(Path)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getFileStoreAttributes(Path)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="lastModifiedInstant(File)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="lastModifiedInstant(Path)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="setAsyncFileStoreAttributes(boolean)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="setLastModified(Path, Instant)"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$Attributes">
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getLastModifiedInstant()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$FileStoreAttributes">
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="FileStoreAttributes"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/References.java" type="org.eclipse.jgit.util.References">
|
|
||||||
<filter id="1108344834">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.4"/>
|
|
||||||
<message_argument value="5.5"/>
|
|
||||||
<message_argument value="org.eclipse.jgit.util.References"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/SimpleLruCache.java" type="org.eclipse.jgit.util.SimpleLruCache">
|
|
||||||
<filter id="1109393411">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="org.eclipse.jgit.util.SimpleLruCache"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/Stats.java" type="org.eclipse.jgit.util.Stats">
|
|
||||||
<filter id="1109393411">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="org.eclipse.jgit.util.Stats"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
</resource>
|
|
||||||
<resource path="src/org/eclipse/jgit/util/SystemReader.java" type="org.eclipse.jgit.util.SystemReader">
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getSystemConfig()"/>
|
|
||||||
</message_arguments>
|
|
||||||
</filter>
|
|
||||||
<filter id="1142947843">
|
|
||||||
<message_arguments>
|
|
||||||
<message_argument value="5.1.9"/>
|
|
||||||
<message_argument value="getUserConfig()"/>
|
|
||||||
</message_arguments>
|
</message_arguments>
|
||||||
</filter>
|
</filter>
|
||||||
</resource>
|
</resource>
|
||||||
|
|
|
@ -51,10 +51,10 @@
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -364,65 +364,72 @@ private Map<String, LockFile> lockLooseRefs(List<ReceiveCommand> commands)
|
||||||
|
|
||||||
private static RefList<Ref> applyUpdates(RevWalk walk, RefList<Ref> refs,
|
private static RefList<Ref> applyUpdates(RevWalk walk, RefList<Ref> refs,
|
||||||
List<ReceiveCommand> commands) throws IOException {
|
List<ReceiveCommand> commands) throws IOException {
|
||||||
int nDeletes = 0;
|
// Construct a new RefList by merging the old list with the updates.
|
||||||
List<ReceiveCommand> adds = new ArrayList<>(commands.size());
|
// This assumes that each ref occurs at most once as a ReceiveCommand.
|
||||||
|
Collections.sort(commands, new Comparator<ReceiveCommand>() {
|
||||||
|
@Override
|
||||||
|
public int compare(ReceiveCommand a, ReceiveCommand b) {
|
||||||
|
return a.getRefName().compareTo(b.getRefName());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
int delta = 0;
|
||||||
for (ReceiveCommand c : commands) {
|
for (ReceiveCommand c : commands) {
|
||||||
if (c.getType() == ReceiveCommand.Type.CREATE) {
|
switch (c.getType()) {
|
||||||
adds.add(c);
|
case DELETE:
|
||||||
} else if (c.getType() == ReceiveCommand.Type.DELETE) {
|
delta--;
|
||||||
nDeletes++;
|
break;
|
||||||
|
case CREATE:
|
||||||
|
delta++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int addIdx = 0;
|
|
||||||
|
|
||||||
// Construct a new RefList by linearly scanning the old list, and merging in
|
RefList.Builder<Ref> b = new RefList.Builder<>(refs.size() + delta);
|
||||||
// any updates.
|
int refIdx = 0;
|
||||||
Map<String, ReceiveCommand> byName = byName(commands);
|
int cmdIdx = 0;
|
||||||
RefList.Builder<Ref> b =
|
while (refIdx < refs.size() || cmdIdx < commands.size()) {
|
||||||
new RefList.Builder<>(refs.size() - nDeletes + adds.size());
|
Ref ref = (refIdx < refs.size()) ? refs.get(refIdx) : null;
|
||||||
for (Ref ref : refs) {
|
ReceiveCommand cmd = (cmdIdx < commands.size())
|
||||||
String name = ref.getName();
|
? commands.get(cmdIdx)
|
||||||
ReceiveCommand cmd = byName.remove(name);
|
: null;
|
||||||
if (cmd == 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmp < 0) {
|
||||||
b.add(ref);
|
b.add(ref);
|
||||||
continue;
|
refIdx++;
|
||||||
}
|
} else if (cmp > 0) {
|
||||||
if (!cmd.getOldId().equals(ref.getObjectId())) {
|
assert cmd != null;
|
||||||
lockFailure(cmd, commands);
|
if (cmd.getType() != ReceiveCommand.Type.CREATE) {
|
||||||
return null;
|
lockFailure(cmd, commands);
|
||||||
}
|
return null;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
addIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd.getType() != ReceiveCommand.Type.DELETE) {
|
|
||||||
b.add(peeledRef(walk, cmd));
|
b.add(peeledRef(walk, cmd));
|
||||||
|
cmdIdx++;
|
||||||
|
} else {
|
||||||
|
assert cmd != null;
|
||||||
|
assert ref != null;
|
||||||
|
if (!cmd.getOldId().equals(ref.getObjectId())) {
|
||||||
|
lockFailure(cmd, commands);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.getType() != ReceiveCommand.Type.DELETE) {
|
||||||
|
b.add(peeledRef(walk, cmd));
|
||||||
|
}
|
||||||
|
cmdIdx++;
|
||||||
|
refIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.toRefList();
|
return b.toRefList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,15 +508,6 @@ private String toResultString(ReceiveCommand cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, ReceiveCommand> byName(
|
|
||||||
List<ReceiveCommand> commands) {
|
|
||||||
Map<String, ReceiveCommand> ret = new LinkedHashMap<>();
|
|
||||||
for (ReceiveCommand cmd : commands) {
|
|
||||||
ret.put(cmd.getRefName(), cmd);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
|
private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
ObjectId newId = cmd.getNewId().copy();
|
ObjectId newId = cmd.getNewId().copy();
|
||||||
|
|
|
@ -101,6 +101,13 @@ private SshConstants() {
|
||||||
/** Key in an ssh config file. */
|
/** Key in an ssh config file. */
|
||||||
public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile";
|
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. */
|
/** Key in an ssh config file. */
|
||||||
public static final String HOST = "Host";
|
public static final String HOST = "Host";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue