Browse Source

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>
next
Matthias Sohn 5 years ago
parent
commit
4d78215673
  1. 2
      WORKSPACE
  2. 12
      org.eclipse.jgit.lfs/.settings/.api_filters
  3. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
  4. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
  5. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
  6. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
  7. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
  8. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
  9. 4
      org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
  10. 10
      org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
  11. 53
      org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
  12. 221
      org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java
  13. 28
      org.eclipse.jgit.ssh.apache/.settings/.api_filters
  14. 6
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
  15. 190
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java
  16. 4
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
  17. 16
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
  18. 420
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
  19. 6
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java
  20. 4
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java
  21. 177
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java
  22. 53
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
  23. 2
      org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
  24. 29
      org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
  25. 161
      org.eclipse.jgit/.settings/.api_filters
  26. 118
      org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
  27. 7
      org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

2
WORKSPACE

@ -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",

12
org.eclipse.jgit.lfs/.settings/.api_filters

@ -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>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml

@ -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

4
org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml

@ -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>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml

@ -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>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml

@ -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>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml

@ -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

4
org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml

@ -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>

4
org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml

@ -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>

10
org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF

@ -7,7 +7,15 @@ Bundle-Version: 5.6.0.qualifier
Bundle-Vendor: %Bundle-Vendor Bundle-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)",

53
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java

@ -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 class ApacheSshTest extends SshTestBase {
"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

221
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java

@ -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());
}
}

28
org.eclipse.jgit.ssh.apache/.settings/.api_filters

@ -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>

6
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java

@ -57,7 +57,6 @@ import java.util.Set;
import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.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 @@ public class JGitClientSession extends ClientSessionImpl {
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) {

190
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java

@ -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();
}
}
}

4
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java

@ -177,6 +177,10 @@ public class JGitSshClient extends SshClient {
return remoteAddress; 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(

16
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java

@ -83,7 +83,9 @@ import org.eclipse.jgit.transport.SshConstants;
*/ */
public class JGitSshConfig implements HostConfigEntryResolver { 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 class JGitSshConfig implements HostConfigEntryResolver {
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);

420
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java → org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java

@ -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 static java.text.MessageFormat.format;
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.Path;
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 @@ import org.slf4j.LoggerFactory;
* @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 @@ public class OpenSshServerKeyVerifier
* 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 class OpenSshServerKeyVerifier
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 class OpenSshServerKeyVerifier
} }
@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 class OpenSshServerKeyVerifier
} }
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 class OpenSshServerKeyVerifier
// 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 @@ public class OpenSshServerKeyVerifier
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 @@ public class OpenSshServerKeyVerifier
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 @@ public class OpenSshServerKeyVerifier
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 @@ public class OpenSshServerKeyVerifier
} }
} }
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 @@ public class OpenSshServerKeyVerifier
} }
} }
private static CredentialsProvider getCredentialsProvider( private static class AskUser {
ClientSession session) {
if (session instanceof JGitClientSession) {
return ((JGitClientSession) session).getCredentialsProvider();
}
return null;
}
private static boolean askUser(CredentialsProvider provider, URIish uri, public enum ModifiedKeyHandling {
String prompt, String... messages) { DENY, ALLOW, ALLOW_AND_STORE
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 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 class OpenSshServerKeyVerifier
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 class OpenSshServerKeyVerifier
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 class OpenSshServerKeyVerifier
} }
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 class OpenSshServerKeyVerifier
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 class OpenSshServerKeyVerifier
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 @@ public class OpenSshServerKeyVerifier
} }
} }
// 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 private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
protected KnownHostEntry prepareKnownHostEntry( String host = null;
ClientSession clientSession, SocketAddress remoteAddress, int port = 0;
PublicKey serverKey) throws IOException { if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
// Make this method accessible .charAt(0)) {
try { int end = address.indexOf(
return super.prepareKnownHostEntry(clientSession, remoteAddress, HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
serverKey); if (end <= 1) {
} catch (Exception e) { return null; // Invalid
throw new IOException(e.getMessage(), e);
} }
host = address.substring(1, end);
if (end < address.length() - 1
&& HostPatternsHolder.PORT_VALUE_DELIMITER == address
.charAt(end + 1)) {
port = parsePort(address.substring(end + 2));
}
} else {
int i = address
.lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER);
if (i > 0) {
port = parsePort(address.substring(i + 1));
host = address.substring(0, i);
} else {
host = address;
}
}
if (port < 0 || port > 65535) {
return null;
} }
return new SshdSocketAddress(host, port);
}
@Override private Collection<SshdSocketAddress> getCandidates(
protected String prepareModifiedServerKeyLine( @NonNull String connectAddress,
ClientSession clientSession, SocketAddress remoteAddress, @NonNull InetSocketAddress remoteAddress) {
KnownHostEntry entry, String curLine, PublicKey expected, Collection<SshdSocketAddress> candidates = new TreeSet<>(
PublicKey actual) throws IOException { SshdSocketAddress.BY_HOST_AND_PORT);
// Make this method accessible candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
try { SshdSocketAddress address = toSshdSocketAddress(connectAddress);
return super.prepareModifiedServerKeyLine(clientSession, if (address != null) {
remoteAddress, entry, curLine, expected, actual); candidates.add(address);
} catch (Exception e) { }
throw new IOException(e.getMessage(), e); 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();
}
@Override private String updateHostKeyLine(String line, PublicKey newKey)
protected Collection<SshdSocketAddress> resolveHostNetworkIdentities( throws IOException {
ClientSession clientSession, SocketAddress remoteAddress) { // Replaces an existing public key by the new key
// Make this method accessible int pos = line.indexOf(' ');
return super.resolveHostNetworkIdentities(clientSession, if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
remoteAddress); // 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();
} }
} }

6
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java

@ -43,9 +43,9 @@
package org.eclipse.jgit.internal.transport.sshd; 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 @@ import org.eclipse.jgit.annotations.NonNull;
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);
} }

4
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java

@ -62,8 +62,8 @@ public class DefaultProxyDataFactory implements ProxyDataFactory {
public ProxyData get(InetSocketAddress remoteAddress) { 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()

177
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java

@ -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();
}
}

53
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java

@ -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.UserAuth;
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.errors.TransportException;
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 class SshdSessionFactory extends SshSessionFactory implements Closeable {
.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 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
@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 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* 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() {

2
org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java

@ -305,7 +305,7 @@ public abstract class SshTestBase extends SshTestHarness {
// without provider. If it works, the server host key was written // 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, //

29
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java

@ -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 static org.junit.Assume.assumeTrue;
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 class BatchRefUpdateTest extends LocalDiskRepositoryTestCase {
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);

161
org.eclipse.jgit/.settings/.api_filters

@ -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="isEqual(AnyObjectId, AnyObjectId)"/>
</message_arguments>
</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="5.5"/>
<message_argument value="org.eclipse.jgit.util.References"/> <message_argument value="5.6"/>
</message_arguments> <message_argument value="HASH_KNOWN_HOSTS"/>
</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>

118
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java

@ -51,10 +51,10 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_RE
import java.io.IOException; import java.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 @@ class PackedBatchRefUpdate extends BatchRefUpdate {
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;
RefList.Builder<Ref> b = new RefList.Builder<>(refs.size() + delta);
// Construct a new RefList by linearly scanning the old list, and merging in int refIdx = 0;
// any updates. int cmdIdx = 0;
Map<String, ReceiveCommand> byName = byName(commands); while (refIdx < refs.size() || cmdIdx < commands.size()) {
RefList.Builder<Ref> b = Ref ref = (refIdx < refs.size()) ? refs.get(refIdx) : null;
new RefList.Builder<>(refs.size() - nDeletes + adds.size()); ReceiveCommand cmd = (cmdIdx < commands.size())
for (Ref ref : refs) { ? commands.get(cmdIdx)
String name = ref.getName(); : null;
ReceiveCommand cmd = byName.remove(name); int cmp = 0;
if (cmd == null) { if (ref != null && cmd != null) {
b.add(ref); cmp = ref.getName().compareTo(cmd.getRefName());
continue; } else if (ref == null) {
} cmp = 1;
if (!cmd.getOldId().equals(ref.getObjectId())) { } else if (cmd == null) {
lockFailure(cmd, commands); cmp = -1;
return null;
} }
// Consume any adds between the last and current ref. if (cmp < 0) {
while (addIdx < adds.size()) { b.add(ref);
ReceiveCommand currAdd = adds.get(addIdx); refIdx++;
if (currAdd.getRefName().compareTo(name) < 0) { } else if (cmp > 0) {
b.add(peeledRef(walk, currAdd)); assert cmd != null;
byName.remove(currAdd.getRefName()); if (cmd.getType() != ReceiveCommand.Type.CREATE) {
} else { lockFailure(cmd, commands);
break; return null;
} }
addIdx++;
}
if (cmd.getType() != ReceiveCommand.Type.DELETE) {
b.add(peeledRef(walk, cmd)); b.add(peeledRef(walk, cmd));
} cmdIdx++;
} } else {
assert cmd != null;
// All remaining adds are valid, since the refs didn't exist. assert ref != null;
while (addIdx < adds.size()) { if (!cmd.getOldId().equals(ref.getObjectId())) {
ReceiveCommand cmd = adds.get(addIdx++); lockFailure(cmd, commands);
byName.remove(cmd.getRefName()); return null;
b.add(peeledRef(walk, cmd)); }
}
// Any remaining updates/deletes do not correspond to any existing refs, so if (cmd.getType() != ReceiveCommand.Type.DELETE) {
// they are lock failures. b.add(peeledRef(walk, cmd));
if (!byName.isEmpty()) { }
lockFailure(byName.values().iterator().next(), commands); cmdIdx++;
return null; refIdx++;
}
} }
return b.toRefList(); return b.toRefList();
} }
@ -501,15 +508,6 @@ class PackedBatchRefUpdate extends BatchRefUpdate {
} }
} }
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();

7
org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

@ -101,6 +101,13 @@ public final class 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…
Cancel
Save