From 08b0a8632d54a24d92075b94d0b0134b69146ba2 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 14 Sep 2018 22:40:08 +0200 Subject: [PATCH] Ssh tests with an Apache MINA sshd test git server 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 --- WORKSPACE | 12 + lib/BUILD | 18 + org.eclipse.jgit.junit/BUILD | 2 + org.eclipse.jgit.junit/META-INF/MANIFEST.MF | 29 +- org.eclipse.jgit.junit/pom.xml | 12 + .../jgit/junit/ssh/SshTestGitServer.java | 217 ++++++++ org.eclipse.jgit.test/BUILD | 4 + org.eclipse.jgit.test/META-INF/MANIFEST.MF | 2 + org.eclipse.jgit.test/pom.xml | 12 + org.eclipse.jgit.test/tests.bzl | 8 +- .../org/eclipse/jgit/transport/id_dsa_test | 15 + .../eclipse/jgit/transport/id_dsa_test.pub | 1 + .../eclipse/jgit/transport/JSchSshTest.java | 113 +++++ .../jgit/transport/OpenSshConfigTest.java | 15 + .../eclipse/jgit/transport/SshTestBase.java | 466 ++++++++++++++++++ pom.xml | 1 + 16 files changed, 922 insertions(+), 5 deletions(-) create mode 100644 org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java diff --git a/WORKSPACE b/WORKSPACE index d327e13afd..30fbfd671c 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -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", diff --git a/lib/BUILD b/lib/BUILD index 7cd420a526..35cb79eb39 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -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"], diff --git a/org.eclipse.jgit.junit/BUILD b/org.eclipse.jgit.junit/BUILD index 74498fdf62..cba2318df4 100644 --- a/org.eclipse.jgit.junit/BUILD +++ b/org.eclipse.jgit.junit/BUILD @@ -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", ], diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF index 9721c42e50..e44ee0301e 100644 --- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF @@ -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" diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml index 24e2c71f9b..112c73f652 100644 --- a/org.eclipse.jgit.junit/pom.xml +++ b/org.eclipse.jgit.junit/pom.xml @@ -73,6 +73,18 @@ ${project.version} + + org.apache.sshd + sshd-core + ${apache-sshd-version} + + + + org.apache.sshd + sshd-sftp + ${apache-sshd-version} + + junit junit diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java new file mode 100644 index 0000000000..675a11589d --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.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 test server based on Apache MINA sshd. + *

+ * Supports only a single repository. Authenticates only the given test user + * against his given test public key. ssh is limited to fetching (upload-pack). + *

+ * + * @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 test 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 + * private 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 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()); + } + } + + } +} diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD index 186de25d94..07597f3251 100644 --- a/org.eclipse.jgit.test/BUILD +++ b/org.eclipse.jgit.test/BUILD @@ -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", ], diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 6df9e39094..6514277ac5 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -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)", diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml index 6aa34f5b76..3d5df9a68a 100644 --- a/org.eclipse.jgit.test/pom.xml +++ b/org.eclipse.jgit.test/pom.xml @@ -112,6 +112,18 @@ org.eclipse.jgit.pgm ${project.version}
+ + + org.apache.sshd + sshd-core + ${apache-sshd-version} + + + + org.apache.sshd + sshd-sftp + ${apache-sshd-version} + diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl index bc06e3ef40..b9ad8b2126 100644 --- a/org.eclipse.jgit.test/tests.bzl +++ b/org.eclipse.jgit.test/tests.bzl @@ -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" diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test new file mode 100644 index 0000000000..cc39e8b870 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test @@ -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----- diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub new file mode 100644 index 0000000000..0528a928c9 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAIsXi0EUiI6GmhHqrwwjvO2wdujW46+uXM/SG2GVI3KxCSf95B2XgXBsgiKH0sy3guyqjDcP4Ph5Mctg1IxqmqugN6xf9YB6lf09bRdIbumVGU6nXW7bZDHdk9nmvWy56vurofwvhoRnQBUJ3L4n7dxxvXhIyRPOxptayOS2ZcnRAAAAFQDsgGxVxcBBM9y0Rm3kNz/R64CYEQAAAIEAgCbyCJNZb66KQBMO7B+NPxx0caSKjZ+3TpWL6pLJGTAu1pztd1wpElECNCEBhTX9p1HEypTIjOUFU2gjgaBLUcWE0JK+/4vJjjvaENvrQardH0EeRfrazhpRY+X6ytUTk0YPDuQn+ZqBhXxAoD8BA+TJMvk7oMpMUTyr6LGBuj4AAACAeXCfOrKY6wHuMkHHpa9Ix95T+7h5ZrSosrV1WO5g9X04LNiPFRXvGyMWYF17VaGqVWID5NbbGP4PqwSw0rjmw7c/xxV2DYNfJ5NFWsDHxhI6RP9AaGTKcdIEykWEkGgJDiVF/DJgjvapGCW4Lo5UB1JJRXEM4YmTiEbyUyahKqw= thomas@Arcturus diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java new file mode 100644 index 0000000000..7b0344070e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +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); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index 19fcbfd7a2..0760585761 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -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"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java new file mode 100644 index 0000000000..0d625f319d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +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> 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 stringStore; + + private final Iterator 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> log = new LinkedHashMap<>(); + + private void logItems(URIish uri, CredentialItem... items) { + log.put(uri, Arrays.asList(items)); + } + + public Map> getLog() { + return log; + } + } +} diff --git a/pom.xml b/pom.xml index 4bd9b3878d..d7daae9878 100644 --- a/pom.xml +++ b/pom.xml @@ -198,6 +198,7 @@ ${project.build.directory}/META-INF/MANIFEST.MF 4.11.0.201803080745-r + 2.0.0 0.1.54 1.1.1 1.1.6 -- 2.39.5