Add a simple ssh git server based on Apache MINA sshd, and use it in new tests that verify ssh operations and in particular a number of bugs that had cropped up over time in JSch. The git server supports fetching only, and sftp access. The tests are all in an abstract base class; the concrete JschSshTest class only provides ssh-specific test setup. So the same tests could be run easily also with some other ssh client. Bug: 520927 Change-Id: Ide6687b717fb497a29fc83f22b07390a26dfce1d Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>tags/v5.2.0.201811281532-m3
@@ -52,6 +52,18 @@ maven_jar( | |||
sha1 = "a86ce739e5a7175b4b234c290a00a5fdb80957a0", | |||
) | |||
maven_jar( | |||
name = "sshd-core", | |||
artifact = "org.apache.sshd:sshd-core:2.0.0", | |||
sha1 = "f4275079a2463cfd2bf1548a80e1683288a8e86b", | |||
) | |||
maven_jar( | |||
name = "sshd-sftp", | |||
artifact = "org.apache.sshd:sshd-sftp:2.0.0", | |||
sha1 = "a12d64dc2d5d23271a4dc58075e55f9c64a68494", | |||
) | |||
maven_jar( | |||
name = "commons-codec", | |||
artifact = "commons-codec:commons-codec:1.10", |
@@ -58,6 +58,24 @@ java_library( | |||
exports = ["@httpcore//jar"], | |||
) | |||
java_library( | |||
name = "sshd-core", | |||
visibility = [ | |||
"//org.eclipse.jgit.junit:__pkg__", | |||
"//org.eclipse.jgit.test:__pkg__", | |||
], | |||
exports = ["@sshd-core//jar"], | |||
) | |||
java_library( | |||
name = "sshd-sftp", | |||
visibility = [ | |||
"//org.eclipse.jgit.junit:__pkg__", | |||
"//org.eclipse.jgit.test:__pkg__", | |||
], | |||
exports = ["@sshd-sftp//jar"], | |||
) | |||
java_library( | |||
name = "javaewah", | |||
visibility = ["//visibility:public"], |
@@ -8,6 +8,8 @@ java_library( | |||
resources = glob(["resources/**"]), | |||
deps = [ | |||
"//lib:junit", | |||
"//lib:sshd-core", | |||
"//lib:sshd-sftp", | |||
# We want these deps to be provided_deps | |||
"//org.eclipse.jgit:jgit", | |||
], |
@@ -8,7 +8,21 @@ Bundle-Localization: plugin | |||
Bundle-Vendor: %provider_name | |||
Bundle-ActivationPolicy: lazy | |||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 | |||
Import-Package: org.eclipse.jgit.api;version="[5.2.0,5.3.0)", | |||
Import-Package: org.apache.sshd.common;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.config.keys;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.file.virtualfs;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.helpers;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.kex;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.keyprovider;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.session;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.util.logging;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.common.util.security;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.server;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.server.command;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.server.shell;version="[2.0.0,2.1.0)", | |||
org.apache.sshd.server.subsystem.sftp;version="[2.0.0,2.1.0)", | |||
org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.api;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.api.errors;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.dircache;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.errors;version="[5.2.0,5.3.0)", | |||
@@ -18,6 +32,7 @@ Import-Package: org.eclipse.jgit.api;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.merge;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.revwalk;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.storage.file;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.transport;version="5.2.0", | |||
org.eclipse.jgit.treewalk;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.treewalk.filter;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.util;version="[5.2.0,5.3.0)", | |||
@@ -26,7 +41,8 @@ Import-Package: org.eclipse.jgit.api;version="[5.2.0,5.3.0)", | |||
org.junit;version="[4.12,5.0.0)", | |||
org.junit.rules;version="[4.12,5.0.0)", | |||
org.junit.runner;version="[4.12,5.0.0)", | |||
org.junit.runners.model;version="[4.12,5.0.0)" | |||
org.junit.runners.model;version="[4.12,5.0.0)", | |||
org.slf4j;version="[1.7.0,2.0.0)" | |||
Export-Package: org.eclipse.jgit.junit;version="5.2.0"; | |||
uses:="org.eclipse.jgit.dircache, | |||
org.eclipse.jgit.lib, | |||
@@ -35,5 +51,10 @@ Export-Package: org.eclipse.jgit.junit;version="5.2.0"; | |||
org.eclipse.jgit.treewalk, | |||
org.eclipse.jgit.util, | |||
org.eclipse.jgit.storage.file, | |||
org.eclipse.jgit.api", | |||
org.eclipse.jgit.junit.time;version="5.2.0" | |||
org.eclipse.jgit.api, | |||
org.junit.rules, | |||
org.junit.runners.model, | |||
org.junit.runner, | |||
org.eclipse.jgit.util.time", | |||
org.eclipse.jgit.junit.ssh;version="5.2.0", | |||
org.eclipse.jgit.junit.time;version="5.2.0";uses:="org.eclipse.jgit.util.time" |
@@ -73,6 +73,18 @@ | |||
<version>${project.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.sshd</groupId> | |||
<artifactId>sshd-core</artifactId> | |||
<version>${apache-sshd-version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.sshd</groupId> | |||
<artifactId>sshd-sftp</artifactId> | |||
<version>${apache-sshd-version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> |
@@ -0,0 +1,217 @@ | |||
/* | |||
* Copyright (C) 2018, 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.junit.ssh; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.IOException; | |||
import java.nio.file.Path; | |||
import java.security.GeneralSecurityException; | |||
import java.security.KeyPair; | |||
import java.security.PublicKey; | |||
import java.text.MessageFormat; | |||
import java.util.Collections; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import org.apache.sshd.common.config.keys.IdentityUtils; | |||
import org.apache.sshd.common.config.keys.KeyUtils; | |||
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; | |||
import org.apache.sshd.common.keyprovider.KeyPairProvider; | |||
import org.apache.sshd.common.session.Session; | |||
import org.apache.sshd.common.util.security.SecurityUtils; | |||
import org.apache.sshd.server.SshServer; | |||
import org.apache.sshd.server.command.AbstractCommandSupport; | |||
import org.apache.sshd.server.shell.UnknownCommand; | |||
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; | |||
import org.eclipse.jgit.annotations.NonNull; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.transport.UploadPack; | |||
/** | |||
* A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd. | |||
* <p> | |||
* Supports only a single repository. Authenticates only the given test user | |||
* against his given test public key. ssh is limited to fetching (upload-pack). | |||
* </p> | |||
* | |||
* @since 5.2 | |||
*/ | |||
public class SshTestGitServer { | |||
@NonNull | |||
private String testUser; | |||
@NonNull | |||
private PublicKey testKey; | |||
@NonNull | |||
private Repository repository; | |||
private final ExecutorService executorService = Executors | |||
.newFixedThreadPool(2); | |||
private final SshServer server; | |||
/** | |||
* Creates a ssh git <em>test</em> server. It serves one single repository, | |||
* and accepts public-key authentication for exactly one test user. | |||
* | |||
* @param testUser | |||
* user name of the test user | |||
* @param testKey | |||
* <em>private</em> key file of the test user; the server will | |||
* only user the public key from it | |||
* @param repository | |||
* to serve | |||
* @param hostKey | |||
* the unencrypted private key to use as host key | |||
* @throws IOException | |||
* @throws GeneralSecurityException | |||
*/ | |||
public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey, | |||
@NonNull Repository repository, @NonNull byte[] hostKey) | |||
throws IOException, GeneralSecurityException { | |||
this.testUser = testUser; | |||
this.testKey = IdentityUtils | |||
.loadIdentities(Collections.singletonMap("A", testKey), null) | |||
.get("A").getPublic(); | |||
this.repository = repository; | |||
server = SshServer.setUpDefaultServer(); | |||
// Set host key | |||
server.setKeyPairProvider(new KeyPairProvider() { | |||
@Override | |||
public Iterable<KeyPair> loadKeys() { | |||
try (ByteArrayInputStream in = new ByteArrayInputStream( | |||
hostKey)) { | |||
return Collections.singletonList( | |||
SecurityUtils.loadKeyPairIdentity("", in, null)); | |||
} catch (IOException | GeneralSecurityException e) { | |||
return null; | |||
} | |||
} | |||
}); | |||
// SFTP. | |||
server.setFileSystemFactory(new VirtualFileSystemFactory() { | |||
@Override | |||
protected Path computeRootDir(Session session) throws IOException { | |||
return SshTestGitServer.this.repository.getDirectory() | |||
.getParentFile().getAbsoluteFile().toPath(); | |||
} | |||
}); | |||
server.setSubsystemFactories(Collections | |||
.singletonList((new SftpSubsystemFactory.Builder()).build())); | |||
// No shell | |||
server.setShellFactory(null); | |||
// Disable some authentications | |||
server.setPasswordAuthenticator(null); | |||
server.setKeyboardInteractiveAuthenticator(null); | |||
server.setGSSAuthenticator(null); | |||
server.setHostBasedAuthenticator(null); | |||
// Accept only the test user/public key | |||
server.setPublickeyAuthenticator((userName, publicKey, session) -> { | |||
return SshTestGitServer.this.testUser.equals(userName) && KeyUtils | |||
.compareKeys(SshTestGitServer.this.testKey, publicKey); | |||
}); | |||
server.setCommandFactory(command -> { | |||
if (command.startsWith("git-upload-pack") | |||
|| command.startsWith("git upload-pack")) { | |||
return new GitUploadPackCommand(command, executorService); | |||
} | |||
return new UnknownCommand(command); | |||
}); | |||
} | |||
/** | |||
* Starts the test server, listening on a random port. | |||
* | |||
* @return the port the server listens on; test clients should connect to | |||
* that port | |||
* @throws IOException | |||
*/ | |||
public int start() throws IOException { | |||
server.start(); | |||
return server.getPort(); | |||
} | |||
/** | |||
* Stops the test server. | |||
* | |||
* @throws IOException | |||
*/ | |||
public void stop() throws IOException { | |||
executorService.shutdownNow(); | |||
server.stop(true); | |||
} | |||
private class GitUploadPackCommand extends AbstractCommandSupport { | |||
protected GitUploadPackCommand(String command, | |||
ExecutorService executorService) { | |||
super(command, executorService, false); | |||
} | |||
@Override | |||
public void run() { | |||
UploadPack uploadPack = new UploadPack(repository); | |||
String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL"); | |||
if (gitProtocol != null) { | |||
uploadPack | |||
.setExtraParameters(Collections.singleton(gitProtocol)); | |||
} | |||
try { | |||
uploadPack.upload(getInputStream(), getOutputStream(), | |||
getErrorStream()); | |||
onExit(0); | |||
} catch (IOException e) { | |||
log.warn( | |||
MessageFormat.format("Could not run {0}", getCommand()), | |||
e); | |||
onExit(-1, e.toString()); | |||
} | |||
} | |||
} | |||
} |
@@ -20,6 +20,7 @@ HELPERS = glob(["src/**/*.java"]) + [PKG + c for c in [ | |||
"revwalk/RevWalkTestCase.java", | |||
"transport/ObjectIdMatcher.java", | |||
"transport/SpiTransport.java", | |||
"transport/SshTestBase.java", | |||
"treewalk/FileTreeIteratorWithTimeControl.java", | |||
"treewalk/filter/AlwaysCloneTreeFilter.java", | |||
"test/resources/SampleDataRepositoryTestCase.java", | |||
@@ -44,6 +45,9 @@ java_library( | |||
resources = DATA, | |||
deps = [ | |||
"//lib:junit", | |||
"//lib:jsch", | |||
"//lib:sshd-core", | |||
"//lib:sshd-sftp", | |||
"//org.eclipse.jgit:jgit", | |||
"//org.eclipse.jgit.junit:junit", | |||
], |
@@ -10,6 +10,7 @@ Bundle-ActivationPolicy: lazy | |||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 | |||
Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", | |||
com.jcraft.jsch;version="[0.1.54,0.2.0)", | |||
org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.api;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.api.errors;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.attributes;version="[5.2.0,5.3.0)", | |||
@@ -34,6 +35,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", | |||
org.eclipse.jgit.internal.storage.reftree;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.internal.transport.parser;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.junit;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.junit.ssh;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.lfs;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.lib;version="[5.2.0,5.3.0)", | |||
org.eclipse.jgit.merge;version="[5.2.0,5.3.0)", |
@@ -112,6 +112,18 @@ | |||
<artifactId>org.eclipse.jgit.pgm</artifactId> | |||
<version>${project.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.sshd</groupId> | |||
<artifactId>sshd-core</artifactId> | |||
<version>${apache-sshd-version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.sshd</groupId> | |||
<artifactId>sshd-sftp</artifactId> | |||
<version>${apache-sshd-version}</version> | |||
</dependency> | |||
</dependencies> | |||
<profiles> |
@@ -41,7 +41,13 @@ def tests(tests): | |||
additional_deps = [ | |||
"//lib:jsch", | |||
] | |||
if src.endswith("JSchSshTest.java"): | |||
additional_deps = [ | |||
"//lib:jsch", | |||
"//lib:jzlib", | |||
"//lib:sshd-core", | |||
"//lib:sshd-sftp", | |||
] | |||
heap_size = "-Xmx256m" | |||
if src.endswith("HugeCommitMessageTest.java"): | |||
heap_size = "-Xmx512m" |
@@ -0,0 +1,15 @@ | |||
-----BEGIN DSA PRIVATE KEY----- | |||
Proc-Type: 4,ENCRYPTED | |||
DEK-Info: AES-128-CBC,D7B8FC3F4E304A2A22754B068767081F | |||
IewkLt6JyqtPccAsnfeLv7IMlLvgm7tqQSYK1/CLhmDE0aZXViD8sqxLA6dVjmkp | |||
BVyk7EBpp43PnVQYsDcMPnyM8H83vNRDtIQ6fxM1PJafiP7Rbn1k1fDh7DwA48PU | |||
FnT6zZ9aYEKYMto0WIdQ86j/uY+LtYygQDDoZ2ohn2NlpykeyrSp0bDRIoW6sdc5 | |||
+LlfDtq2usv3fcxMAJpO/SSN78LvBlyOK4n/JAVSkPawsW1WsIrXA52mk0iUhjYc | |||
aYOCuL+wA7OmHAOpfS5HUXZ4i/7qONnLBkEqeIOcgTmShh1c4oWw9TjWK1AzdSDU | |||
G2nkRJ/8zK/jdm5wcmrjrzuREM1VbCiXHlVoHYI0W1Z9etOgz1cj4KLz/bB8Nf+8 | |||
shCez1Aw5ec33BzwysfwymfAKeXjYaxdKcur3j+UdXAlYRD28BRnWmTL5Jx82eUu | |||
NIh0U9pHkn+PjdzmjSPEUP7wzDjQQacaQTkBRf5gPyOYfv/+Mnq6YyflKaPYmkEr | |||
eztO22VZlpyp/hj2LzBav9wi0++teInNQGU+GxHedsWm4+YpffMhz1bz5ZUQ670A | |||
0WJJH3k/KnxbCY3usj4eJr+CsX+LNZhm+rKyjRDmRwA= | |||
-----END DSA PRIVATE KEY----- |
@@ -0,0 +1 @@ | |||
ssh-dss AAAAB3NzaC1kc3MAAACBAIsXi0EUiI6GmhHqrwwjvO2wdujW46+uXM/SG2GVI3KxCSf95B2XgXBsgiKH0sy3guyqjDcP4Ph5Mctg1IxqmqugN6xf9YB6lf09bRdIbumVGU6nXW7bZDHdk9nmvWy56vurofwvhoRnQBUJ3L4n7dxxvXhIyRPOxptayOS2ZcnRAAAAFQDsgGxVxcBBM9y0Rm3kNz/R64CYEQAAAIEAgCbyCJNZb66KQBMO7B+NPxx0caSKjZ+3TpWL6pLJGTAu1pztd1wpElECNCEBhTX9p1HEypTIjOUFU2gjgaBLUcWE0JK+/4vJjjvaENvrQardH0EeRfrazhpRY+X6ytUTk0YPDuQn+ZqBhXxAoD8BA+TJMvk7oMpMUTyr6LGBuj4AAACAeXCfOrKY6wHuMkHHpa9Ix95T+7h5ZrSosrV1WO5g9X04LNiPFRXvGyMWYF17VaGqVWID5NbbGP4PqwSw0rjmw7c/xxV2DYNfJ5NFWsDHxhI6RP9AaGTKcdIEykWEkGgJDiVF/DJgjvapGCW4Lo5UB1JJRXEM4YmTiEbyUyahKqw= thomas@Arcturus |
@@ -0,0 +1,113 @@ | |||
/* | |||
* Copyright (C) 2018, 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; | |||
import static org.junit.Assert.assertTrue; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.io.UncheckedIOException; | |||
import java.nio.file.Files; | |||
import java.util.Arrays; | |||
import org.eclipse.jgit.errors.TransportException; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.transport.OpenSshConfig.Host; | |||
import org.eclipse.jgit.util.FS; | |||
import com.jcraft.jsch.JSch; | |||
import com.jcraft.jsch.JSchException; | |||
import com.jcraft.jsch.Session; | |||
public class JSchSshTest extends SshTestBase { | |||
private class TestSshSessionFactory extends JschConfigSessionFactory { | |||
@Override | |||
protected void configure(Host hc, Session session) { | |||
// Nothing | |||
} | |||
@Override | |||
public synchronized RemoteSession getSession(URIish uri, | |||
CredentialsProvider credentialsProvider, FS fs, int tms) | |||
throws TransportException { | |||
return super.getSession(uri, credentialsProvider, fs, tms); | |||
} | |||
@Override | |||
protected JSch createDefaultJSch(FS fs) throws JSchException { | |||
JSch defaultJSch = super.createDefaultJSch(fs); | |||
if (knownHosts.exists()) { | |||
defaultJSch.setKnownHosts(knownHosts.getAbsolutePath()); | |||
} | |||
return defaultJSch; | |||
} | |||
} | |||
@Override | |||
protected SshSessionFactory createSessionFactory() { | |||
return new TestSshSessionFactory(); | |||
} | |||
@Override | |||
protected void installConfig(String... config) { | |||
SshSessionFactory factory = getSessionFactory(); | |||
assertTrue(factory instanceof JschConfigSessionFactory); | |||
JschConfigSessionFactory j = (JschConfigSessionFactory) factory; | |||
try { | |||
j.setConfig(createConfig(config)); | |||
} catch (IOException e) { | |||
throw new UncheckedIOException(e); | |||
} | |||
} | |||
private OpenSshConfig createConfig(String... content) throws IOException { | |||
File configFile = new File(sshDir, Constants.CONFIG); | |||
if (content != null) { | |||
Files.write(configFile.toPath(), Arrays.asList(content)); | |||
} | |||
return new OpenSshConfig(getTemporaryDirectory(), configFile); | |||
} | |||
} |
@@ -67,6 +67,7 @@ import org.junit.Before; | |||
import org.junit.Test; | |||
import com.jcraft.jsch.ConfigRepository; | |||
import com.jcraft.jsch.ConfigRepository.Config; | |||
public class OpenSshConfigTest extends RepositoryTestCase { | |||
private File home; | |||
@@ -163,6 +164,20 @@ public class OpenSshConfigTest extends RepositoryTestCase { | |||
assertEquals("bad.tld\"", osc.lookup("bad").getHostName()); | |||
} | |||
@Test | |||
public void testCaseInsensitiveKeyLookup() throws Exception { | |||
config("Host orcz\n" + "Port 29418\n" | |||
+ "\tHostName repo.or.cz\nStrictHostKeyChecking yes\n"); | |||
final Host h = osc.lookup("orcz"); | |||
Config c = h.getConfig(); | |||
String exactCase = c.getValue("StrictHostKeyChecking"); | |||
assertEquals("yes", exactCase); | |||
assertEquals(exactCase, c.getValue("stricthostkeychecking")); | |||
assertEquals(exactCase, c.getValue("STRICTHOSTKEYCHECKING")); | |||
assertEquals(exactCase, c.getValue("sTrIcThostKEYcheckING")); | |||
assertNull(c.getValue("sTrIcThostKEYcheckIN")); | |||
} | |||
@Test | |||
public void testAlias_DoesNotMatch() throws Exception { | |||
config("Host orcz\n" + "Port 29418\n" + "\tHostName repo.or.cz\n"); |
@@ -0,0 +1,466 @@ | |||
/* | |||
* Copyright (C) 2018, 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; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertNotEquals; | |||
import static org.junit.Assert.assertNotNull; | |||
import static org.junit.Assert.assertTrue; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.File; | |||
import java.io.FileOutputStream; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.nio.charset.StandardCharsets; | |||
import java.nio.file.Files; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.Iterator; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.eclipse.jgit.api.CloneCommand; | |||
import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.api.errors.TransportException; | |||
import org.eclipse.jgit.errors.UnsupportedCredentialItem; | |||
import org.eclipse.jgit.junit.RepositoryTestCase; | |||
import org.eclipse.jgit.junit.ssh.SshTestGitServer; | |||
import org.eclipse.jgit.util.FS; | |||
import org.junit.After; | |||
import org.junit.Test; | |||
import com.jcraft.jsch.JSch; | |||
import com.jcraft.jsch.KeyPair; | |||
public abstract class SshTestBase extends RepositoryTestCase { | |||
protected static final String TEST_USER = "testuser"; | |||
protected File sshDir; | |||
protected File privateKey1; | |||
protected File privateKey2; | |||
private SshTestGitServer server; | |||
private SshSessionFactory factory; | |||
protected int testPort; | |||
protected File knownHosts; | |||
private File homeDir; | |||
@Override | |||
public void setUp() throws Exception { | |||
super.setUp(); | |||
writeTrashFile("file.txt", "something"); | |||
try (Git git = new Git(db)) { | |||
git.add().addFilepattern("file.txt").call(); | |||
git.commit().setMessage("Initial commit").call(); | |||
} | |||
mockSystemReader.setProperty("user.home", | |||
getTemporaryDirectory().getAbsolutePath()); | |||
mockSystemReader.setProperty("HOME", | |||
getTemporaryDirectory().getAbsolutePath()); | |||
homeDir = FS.DETECTED.userHome(); | |||
FS.DETECTED.setUserHome(getTemporaryDirectory().getAbsoluteFile()); | |||
sshDir = new File(getTemporaryDirectory(), ".ssh"); | |||
assertTrue(sshDir.mkdir()); | |||
File serverDir = new File(getTemporaryDirectory(), "srv"); | |||
assertTrue(serverDir.mkdir()); | |||
// Create two key pairs. Let's not call them "id_rsa". | |||
privateKey1 = new File(sshDir, "first_key"); | |||
privateKey2 = new File(sshDir, "second_key"); | |||
createKeyPair(privateKey1); | |||
createKeyPair(privateKey2); | |||
ByteArrayOutputStream publicHostKey = new ByteArrayOutputStream(); | |||
// Start a server with our test user and the first key. | |||
server = new SshTestGitServer(TEST_USER, privateKey1.toPath(), db, | |||
createHostKey(publicHostKey)); | |||
testPort = server.start(); | |||
assertTrue(testPort > 0); | |||
knownHosts = new File(sshDir, "known_hosts"); | |||
Files.write(knownHosts.toPath(), Collections.singleton("[localhost]:" | |||
+ testPort + ' ' | |||
+ publicHostKey.toString(StandardCharsets.US_ASCII.name()))); | |||
factory = createSessionFactory(); | |||
SshSessionFactory.setInstance(factory); | |||
} | |||
private static void createKeyPair(File privateKeyFile) throws Exception { | |||
// Found no way to do this with MINA sshd except rolling it all | |||
// ourselves... | |||
JSch jsch = new JSch(); | |||
KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048); | |||
try (OutputStream out = new FileOutputStream(privateKeyFile)) { | |||
pair.writePrivateKey(out); | |||
} | |||
File publicKeyFile = new File(privateKeyFile.getParentFile(), | |||
privateKeyFile.getName() + ".pub"); | |||
try (OutputStream out = new FileOutputStream(publicKeyFile)) { | |||
pair.writePublicKey(out, TEST_USER); | |||
} | |||
} | |||
private static byte[] createHostKey(OutputStream publicKey) | |||
throws Exception { | |||
JSch jsch = new JSch(); | |||
KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048); | |||
pair.writePublicKey(publicKey, ""); | |||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { | |||
pair.writePrivateKey(out); | |||
out.flush(); | |||
return out.toByteArray(); | |||
} | |||
} | |||
@After | |||
public void shutdownServer() throws Exception { | |||
if (server != null) { | |||
server.stop(); | |||
server = null; | |||
} | |||
FS.DETECTED.setUserHome(homeDir); | |||
SshSessionFactory.setInstance(null); | |||
factory = null; | |||
} | |||
protected abstract SshSessionFactory createSessionFactory(); | |||
protected SshSessionFactory getSessionFactory() { | |||
return factory; | |||
} | |||
protected abstract void installConfig(String... config); | |||
@Test(expected = TransportException.class) | |||
public void testSshCloneWithoutConfig() throws Exception { | |||
cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort | |||
+ "/doesntmatter", null); | |||
} | |||
@Test | |||
public void testSshCloneWithGlobalIdentity() throws Exception { | |||
cloneWith( | |||
"ssh://" + TEST_USER + "@localhost:" + testPort | |||
+ "/doesntmatter", | |||
null, | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithDefaultIdentity() throws Exception { | |||
File idRsa = new File(privateKey1.getParentFile(), "id_rsa"); | |||
Files.copy(privateKey1.toPath(), idRsa.toPath()); | |||
// We expect the session factory to pick up these keys... | |||
cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort | |||
+ "/doesntmatter", null); | |||
} | |||
@Test | |||
public void testSshCloneWithConfig() throws Exception { | |||
cloneWith("ssh://localhost/doesntmatter", null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithConfigEncryptedUnusedKey() throws Exception { | |||
// Copy the encrypted test key from the bundle. | |||
File encryptedKey = new File(sshDir, "id_dsa"); | |||
try (InputStream in = SshTestBase.class | |||
.getResourceAsStream("id_dsa_test")) { | |||
Files.copy(in, encryptedKey.toPath()); | |||
} | |||
TestCredentialsProvider provider = new TestCredentialsProvider( | |||
"testpass"); | |||
cloneWith("ssh://localhost/doesntmatter", provider, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
assertEquals("CredentialsProvider should not have been called", 0, | |||
provider.getLog().size()); | |||
} | |||
@Test(expected = TransportException.class) | |||
public void testSshCloneWithoutKnownHosts() throws Exception { | |||
assertTrue("Could not delete known_hosts", knownHosts.delete()); | |||
cloneWith("ssh://localhost/doesntmatter", null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithoutKnownHostsWithProvider() throws Exception { | |||
File copiedHosts = new File(knownHosts.getParentFile(), | |||
"copiedKnownHosts"); | |||
assertTrue("Failed to rename known_hosts", | |||
knownHosts.renameTo(copiedHosts)); | |||
TestCredentialsProvider provider = new TestCredentialsProvider(); | |||
cloneWith("ssh://localhost/doesntmatter", provider, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
Map<URIish, List<CredentialItem>> messages = provider.getLog(); | |||
assertFalse("Expected user iteraction", messages.isEmpty()); | |||
} | |||
@Test | |||
public void testSftpCloneWithConfig() throws Exception { | |||
cloneWith("sftp://localhost/.git", null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test(expected = TransportException.class) | |||
public void testSshCloneWithConfigWrongKey() throws Exception { | |||
cloneWith("ssh://localhost/doesntmatter", null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey2.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithWrongUserNameInConfig() throws Exception { | |||
// Bug 526778 | |||
cloneWith( | |||
"ssh://" + TEST_USER + "@localhost:" + testPort | |||
+ "/doesntmatter", | |||
null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"User sombody_else", // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithWrongPortInConfig() throws Exception { | |||
// Bug 526778 | |||
cloneWith( | |||
"ssh://" + TEST_USER + "@localhost:" + testPort | |||
+ "/doesntmatter", | |||
null, // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port 22", // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithAliasInConfig() throws Exception { | |||
// Bug 531118 | |||
cloneWith("ssh://git/doesntmatter", null, // | |||
"Host git", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), "", // | |||
"Host localhost", // | |||
"HostName localhost", // | |||
"Port 22", // | |||
"User someone_else", // | |||
"IdentityFile " + privateKey2.getAbsolutePath()); | |||
} | |||
@Test | |||
public void testSshCloneWithUnknownCiphersInConfig() throws Exception { | |||
// Bug 535672 | |||
cloneWith("ssh://git/doesntmatter", null, // | |||
"Host git", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), // | |||
"Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr"); | |||
} | |||
@Test | |||
public void testSshCloneWithUnknownHostKeyAlgorithmsInConfig() | |||
throws Exception { | |||
// Bug 535672 | |||
cloneWith("ssh://git/doesntmatter", null, // | |||
"Host git", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), // | |||
"HostKeyAlgorithms foobar,ssh-rsa,ssh-dss"); | |||
} | |||
@Test | |||
public void testSshCloneWithUnknownKexAlgorithmsInConfig() | |||
throws Exception { | |||
// Bug 535672 | |||
cloneWith("ssh://git/doesntmatter", null, // | |||
"Host git", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), // | |||
"KexAlgorithms foobar,diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521"); | |||
} | |||
@Test | |||
public void testSshCloneWithMinimalHostKeyAlgorithmsInConfig() | |||
throws Exception { | |||
// Bug 537790 | |||
cloneWith("ssh://git/doesntmatter", null, // | |||
"Host git", // | |||
"HostName localhost", // | |||
"Port " + testPort, // | |||
"User " + TEST_USER, // | |||
"IdentityFile " + privateKey1.getAbsolutePath(), // | |||
"HostKeyAlgorithms ssh-rsa,ssh-dss"); | |||
} | |||
private void cloneWith(String uri, CredentialsProvider provider, | |||
String... config) throws Exception { | |||
installConfig(config); | |||
File cloned = new File(getTemporaryDirectory(), "cloned"); | |||
CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true) | |||
.setDirectory(cloned).setURI(uri); | |||
if (provider != null) { | |||
clone.setCredentialsProvider(provider); | |||
} | |||
try (Git git = clone.call()) { | |||
assertNotNull(git.getRepository().resolve("master")); | |||
assertNotEquals(db.getWorkTree(), | |||
git.getRepository().getWorkTree()); | |||
checkFile(new File(git.getRepository().getWorkTree(), "file.txt"), | |||
"something"); | |||
} | |||
} | |||
private class TestCredentialsProvider extends CredentialsProvider { | |||
private final List<String> stringStore; | |||
private final Iterator<String> strings; | |||
public TestCredentialsProvider(String... strings) { | |||
if (strings == null || strings.length == 0) { | |||
stringStore = Collections.emptyList(); | |||
} else { | |||
stringStore = Arrays.asList(strings); | |||
} | |||
this.strings = stringStore.iterator(); | |||
} | |||
@Override | |||
public boolean isInteractive() { | |||
return true; | |||
} | |||
@Override | |||
public boolean supports(CredentialItem... items) { | |||
return true; | |||
} | |||
@Override | |||
public boolean get(URIish uri, CredentialItem... items) | |||
throws UnsupportedCredentialItem { | |||
System.out.println("URI: " + uri); | |||
for (CredentialItem item : items) { | |||
System.out.println(item.getClass().getSimpleName() + ' ' | |||
+ item.getPromptText()); | |||
} | |||
logItems(uri, items); | |||
for (CredentialItem item : items) { | |||
if (item instanceof CredentialItem.InformationalMessage) { | |||
continue; | |||
} | |||
if (item instanceof CredentialItem.YesNoType) { | |||
((CredentialItem.YesNoType) item).setValue(true); | |||
} else if (item instanceof CredentialItem.CharArrayType) { | |||
if (strings.hasNext()) { | |||
((CredentialItem.CharArrayType) item) | |||
.setValue(strings.next().toCharArray()); | |||
} else { | |||
return false; | |||
} | |||
} else if (item instanceof CredentialItem.StringType) { | |||
if (strings.hasNext()) { | |||
((CredentialItem.StringType) item) | |||
.setValue(strings.next()); | |||
} else { | |||
return false; | |||
} | |||
} else { | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
private Map<URIish, List<CredentialItem>> log = new LinkedHashMap<>(); | |||
private void logItems(URIish uri, CredentialItem... items) { | |||
log.put(uri, Arrays.asList(items)); | |||
} | |||
public Map<URIish, List<CredentialItem>> getLog() { | |||
return log; | |||
} | |||
} | |||
} |
@@ -198,6 +198,7 @@ | |||
<bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest> | |||
<jgit-last-release-version>4.11.0.201803080745-r</jgit-last-release-version> | |||
<apache-sshd-version>2.0.0</apache-sshd-version> | |||
<jsch-version>0.1.54</jsch-version> | |||
<jzlib-version>1.1.1</jzlib-version> | |||
<javaewah-version>1.1.6</javaewah-version> |