You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SshTestHarness.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.transport.ssh;
  44. import static org.junit.Assert.assertEquals;
  45. import static org.junit.Assert.assertFalse;
  46. import static org.junit.Assert.assertNotEquals;
  47. import static org.junit.Assert.assertNotNull;
  48. import static org.junit.Assert.assertTrue;
  49. import java.io.ByteArrayOutputStream;
  50. import java.io.File;
  51. import java.io.FileOutputStream;
  52. import java.io.IOException;
  53. import java.io.InputStream;
  54. import java.io.OutputStream;
  55. import java.nio.charset.StandardCharsets;
  56. import java.nio.file.Files;
  57. import java.util.Arrays;
  58. import java.util.Collections;
  59. import java.util.Iterator;
  60. import java.util.LinkedHashMap;
  61. import java.util.List;
  62. import java.util.Map;
  63. import org.eclipse.jgit.api.CloneCommand;
  64. import org.eclipse.jgit.api.Git;
  65. import org.eclipse.jgit.api.PushCommand;
  66. import org.eclipse.jgit.api.ResetCommand.ResetType;
  67. import org.eclipse.jgit.errors.UnsupportedCredentialItem;
  68. import org.eclipse.jgit.junit.RepositoryTestCase;
  69. import org.eclipse.jgit.junit.ssh.SshTestGitServer;
  70. import org.eclipse.jgit.lib.Constants;
  71. import org.eclipse.jgit.lib.Repository;
  72. import org.eclipse.jgit.revwalk.RevCommit;
  73. import org.eclipse.jgit.transport.CredentialItem;
  74. import org.eclipse.jgit.transport.CredentialsProvider;
  75. import org.eclipse.jgit.transport.PushResult;
  76. import org.eclipse.jgit.transport.RemoteRefUpdate;
  77. import org.eclipse.jgit.transport.SshSessionFactory;
  78. import org.eclipse.jgit.transport.URIish;
  79. import org.eclipse.jgit.util.FS;
  80. import org.junit.After;
  81. import com.jcraft.jsch.JSch;
  82. import com.jcraft.jsch.KeyPair;
  83. /**
  84. * Root class for ssh tests. Sets up the ssh test server. A set of pre-computed
  85. * keys for testing is provided in the bundle and can be used in test cases via
  86. * {@link #copyTestResource(String, File)}. These test key files names have four
  87. * components, separated by a single underscore: "id", the algorithm, the bits
  88. * (if variable), and the password if the private key is encrypted. For instance
  89. * "{@code id_ecdsa_384_testpass}" is an encrypted ECDSA-384 key. The passphrase
  90. * to decrypt is "testpass". The key "{@code id_ecdsa_384}" is the same but
  91. * unencrypted. All keys were generated and encrypted via ssh-keygen. Note that
  92. * DSA and ec25519 have no "bits" component. Available keys are listed in
  93. * {@link SshTestBase#KEY_RESOURCES}.
  94. */
  95. public abstract class SshTestHarness extends RepositoryTestCase {
  96. protected static final String TEST_USER = "testuser";
  97. protected File sshDir;
  98. protected File privateKey1;
  99. protected File privateKey2;
  100. protected File publicKey1;
  101. protected SshTestGitServer server;
  102. private SshSessionFactory factory;
  103. protected int testPort;
  104. protected File knownHosts;
  105. private File homeDir;
  106. @Override
  107. public void setUp() throws Exception {
  108. super.setUp();
  109. writeTrashFile("file.txt", "something");
  110. try (Git git = new Git(db)) {
  111. git.add().addFilepattern("file.txt").call();
  112. git.commit().setMessage("Initial commit").call();
  113. }
  114. mockSystemReader.setProperty("user.home",
  115. getTemporaryDirectory().getAbsolutePath());
  116. mockSystemReader.setProperty("HOME",
  117. getTemporaryDirectory().getAbsolutePath());
  118. homeDir = FS.DETECTED.userHome();
  119. FS.DETECTED.setUserHome(getTemporaryDirectory().getAbsoluteFile());
  120. sshDir = new File(getTemporaryDirectory(), ".ssh");
  121. assertTrue(sshDir.mkdir());
  122. File serverDir = new File(getTemporaryDirectory(), "srv");
  123. assertTrue(serverDir.mkdir());
  124. // Create two key pairs. Let's not call them "id_rsa".
  125. privateKey1 = new File(sshDir, "first_key");
  126. privateKey2 = new File(sshDir, "second_key");
  127. publicKey1 = createKeyPair(privateKey1);
  128. createKeyPair(privateKey2);
  129. ByteArrayOutputStream publicHostKey = new ByteArrayOutputStream();
  130. // Start a server with our test user and the first key.
  131. server = new SshTestGitServer(TEST_USER, publicKey1.toPath(), db,
  132. createHostKey(publicHostKey));
  133. testPort = server.start();
  134. assertTrue(testPort > 0);
  135. knownHosts = new File(sshDir, "known_hosts");
  136. Files.write(knownHosts.toPath(), Collections.singleton("[localhost]:"
  137. + testPort + ' '
  138. + publicHostKey.toString(StandardCharsets.US_ASCII.name())));
  139. factory = createSessionFactory();
  140. SshSessionFactory.setInstance(factory);
  141. }
  142. private static File createKeyPair(File privateKeyFile) throws Exception {
  143. // Found no way to do this with MINA sshd except rolling it all
  144. // ourselves...
  145. JSch jsch = new JSch();
  146. KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
  147. try (OutputStream out = new FileOutputStream(privateKeyFile)) {
  148. pair.writePrivateKey(out);
  149. }
  150. File publicKeyFile = new File(privateKeyFile.getParentFile(),
  151. privateKeyFile.getName() + ".pub");
  152. try (OutputStream out = new FileOutputStream(publicKeyFile)) {
  153. pair.writePublicKey(out, TEST_USER);
  154. }
  155. return publicKeyFile;
  156. }
  157. private static byte[] createHostKey(OutputStream publicKey)
  158. throws Exception {
  159. JSch jsch = new JSch();
  160. KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
  161. pair.writePublicKey(publicKey, "");
  162. try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
  163. pair.writePrivateKey(out);
  164. out.flush();
  165. return out.toByteArray();
  166. }
  167. }
  168. /**
  169. * Creates a new known_hosts file with one entry for the given host and port
  170. * taken from the given public key file.
  171. *
  172. * @param file
  173. * to write the known_hosts file to
  174. * @param host
  175. * for the entry
  176. * @param port
  177. * for the entry
  178. * @param publicKey
  179. * to use
  180. * @return the public-key part of the line
  181. * @throws IOException
  182. */
  183. protected static String createKnownHostsFile(File file, String host,
  184. int port, File publicKey) throws IOException {
  185. List<String> lines = Files.readAllLines(publicKey.toPath(),
  186. StandardCharsets.UTF_8);
  187. assertEquals("Public key has too many lines", 1, lines.size());
  188. String pubKey = lines.get(0);
  189. // Strip off the comment.
  190. String[] parts = pubKey.split("\\s+");
  191. assertTrue("Unexpected key content",
  192. parts.length == 2 || parts.length == 3);
  193. String keyPart = parts[0] + ' ' + parts[1];
  194. String line = '[' + host + "]:" + port + ' ' + keyPart;
  195. Files.write(file.toPath(), Collections.singletonList(line));
  196. return keyPart;
  197. }
  198. /**
  199. * Checks whether there is a line for the given host and port that also
  200. * matches the given key part in the list of lines.
  201. *
  202. * @param host
  203. * to look for
  204. * @param port
  205. * to look for
  206. * @param keyPart
  207. * to look for
  208. * @param lines
  209. * to look in
  210. * @return {@code true} if found, {@code false} otherwise
  211. */
  212. protected boolean hasHostKey(String host, int port, String keyPart,
  213. List<String> lines) {
  214. String h = '[' + host + "]:" + port;
  215. return lines.stream()
  216. .anyMatch(l -> l.contains(h) && l.contains(keyPart));
  217. }
  218. @After
  219. public void shutdownServer() throws Exception {
  220. if (server != null) {
  221. server.stop();
  222. server = null;
  223. }
  224. FS.DETECTED.setUserHome(homeDir);
  225. SshSessionFactory.setInstance(null);
  226. factory = null;
  227. }
  228. protected abstract SshSessionFactory createSessionFactory();
  229. protected SshSessionFactory getSessionFactory() {
  230. return factory;
  231. }
  232. protected abstract void installConfig(String... config);
  233. /**
  234. * Copies a test data file contained in the test bundle to the given file.
  235. * Equivalent to {@link #copyTestResource(Class, String, File)} with
  236. * {@code SshTestHarness.class} as first parameter.
  237. *
  238. * @param resourceName
  239. * of the test resource to copy
  240. * @param to
  241. * file to copy the resource to
  242. * @throws IOException
  243. * if the resource cannot be copied
  244. */
  245. protected void copyTestResource(String resourceName, File to)
  246. throws IOException {
  247. copyTestResource(SshTestHarness.class, resourceName, to);
  248. }
  249. /**
  250. * Copies a test data file contained in the test bundle to the given file,
  251. * using {@link Class#getResourceAsStream(String)} to get the test resource.
  252. *
  253. * @param loader
  254. * {@link Class} to use to load the resource
  255. * @param resourceName
  256. * of the test resource to copy
  257. * @param to
  258. * file to copy the resource to
  259. * @throws IOException
  260. * if the resource cannot be copied
  261. */
  262. protected void copyTestResource(Class<?> loader, String resourceName,
  263. File to) throws IOException {
  264. try (InputStream in = loader.getResourceAsStream(resourceName)) {
  265. Files.copy(in, to.toPath());
  266. }
  267. }
  268. protected File cloneWith(String uri, File to, CredentialsProvider provider,
  269. String... config) throws Exception {
  270. installConfig(config);
  271. CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true)
  272. .setDirectory(to).setURI(uri);
  273. if (provider != null) {
  274. clone.setCredentialsProvider(provider);
  275. }
  276. try (Git git = clone.call()) {
  277. Repository repo = git.getRepository();
  278. assertNotNull(repo.resolve("master"));
  279. assertNotEquals(db.getWorkTree(),
  280. git.getRepository().getWorkTree());
  281. assertTrue(new File(git.getRepository().getWorkTree(), "file.txt")
  282. .exists());
  283. return repo.getWorkTree();
  284. }
  285. }
  286. protected void pushTo(File localClone) throws Exception {
  287. pushTo(null, localClone);
  288. }
  289. protected void pushTo(CredentialsProvider provider, File localClone)
  290. throws Exception {
  291. RevCommit commit;
  292. File newFile = null;
  293. try (Git git = Git.open(localClone)) {
  294. // Write a new file and modify a file.
  295. Repository local = git.getRepository();
  296. newFile = File.createTempFile("new", "sshtest",
  297. local.getWorkTree());
  298. write(newFile, "something new");
  299. File existingFile = new File(local.getWorkTree(), "file.txt");
  300. write(existingFile, "something else");
  301. git.add().addFilepattern("file.txt")
  302. .addFilepattern(newFile.getName())
  303. .call();
  304. commit = git.commit().setMessage("Local commit").call();
  305. // Push
  306. PushCommand push = git.push().setPushAll();
  307. if (provider != null) {
  308. push.setCredentialsProvider(provider);
  309. }
  310. Iterable<PushResult> results = push.call();
  311. for (PushResult result : results) {
  312. for (RemoteRefUpdate u : result.getRemoteUpdates()) {
  313. assertEquals(
  314. "Could not update " + u.getRemoteName() + ' '
  315. + u.getMessage(),
  316. RemoteRefUpdate.Status.OK, u.getStatus());
  317. }
  318. }
  319. }
  320. // Now check "master" in the remote repo directly:
  321. assertEquals("Unexpected remote commit", commit, db.resolve("master"));
  322. assertEquals("Unexpected remote commit", commit,
  323. db.resolve(Constants.HEAD));
  324. File remoteFile = new File(db.getWorkTree(), newFile.getName());
  325. assertFalse("File should not exist on remote", remoteFile.exists());
  326. try (Git git = new Git(db)) {
  327. git.reset().setMode(ResetType.HARD).setRef(Constants.HEAD).call();
  328. }
  329. assertTrue("File does not exist on remote", remoteFile.exists());
  330. checkFile(remoteFile, "something new");
  331. }
  332. protected static class TestCredentialsProvider extends CredentialsProvider {
  333. private final List<String> stringStore;
  334. private final Iterator<String> strings;
  335. public TestCredentialsProvider(String... strings) {
  336. if (strings == null || strings.length == 0) {
  337. stringStore = Collections.emptyList();
  338. } else {
  339. stringStore = Arrays.asList(strings);
  340. }
  341. this.strings = stringStore.iterator();
  342. }
  343. @Override
  344. public boolean isInteractive() {
  345. return true;
  346. }
  347. @Override
  348. public boolean supports(CredentialItem... items) {
  349. return true;
  350. }
  351. @Override
  352. public boolean get(URIish uri, CredentialItem... items)
  353. throws UnsupportedCredentialItem {
  354. System.out.println("URI: " + uri);
  355. for (CredentialItem item : items) {
  356. System.out.println(item.getClass().getSimpleName() + ' '
  357. + item.getPromptText());
  358. }
  359. logItems(uri, items);
  360. for (CredentialItem item : items) {
  361. if (item instanceof CredentialItem.InformationalMessage) {
  362. continue;
  363. }
  364. if (item instanceof CredentialItem.YesNoType) {
  365. ((CredentialItem.YesNoType) item).setValue(true);
  366. } else if (item instanceof CredentialItem.CharArrayType) {
  367. if (strings.hasNext()) {
  368. ((CredentialItem.CharArrayType) item)
  369. .setValue(strings.next().toCharArray());
  370. } else {
  371. return false;
  372. }
  373. } else if (item instanceof CredentialItem.StringType) {
  374. if (strings.hasNext()) {
  375. ((CredentialItem.StringType) item)
  376. .setValue(strings.next());
  377. } else {
  378. return false;
  379. }
  380. } else {
  381. return false;
  382. }
  383. }
  384. return true;
  385. }
  386. private Map<URIish, List<CredentialItem>> log = new LinkedHashMap<>();
  387. private void logItems(URIish uri, CredentialItem... items) {
  388. log.put(uri, Arrays.asList(items));
  389. }
  390. public Map<URIish, List<CredentialItem>> getLog() {
  391. return log;
  392. }
  393. }
  394. }