Browse Source
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>stable-5.2
Thomas Wolf
6 years ago
committed by
Matthias Sohn
16 changed files with 922 additions and 5 deletions
@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue