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 13KB

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