Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

SshTestHarness.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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 java.nio.charset.StandardCharsets.US_ASCII;
  45. import static java.nio.charset.StandardCharsets.UTF_8;
  46. import static org.junit.Assert.assertEquals;
  47. import static org.junit.Assert.assertFalse;
  48. import static org.junit.Assert.assertNotEquals;
  49. import static org.junit.Assert.assertNotNull;
  50. import static org.junit.Assert.assertTrue;
  51. import java.io.ByteArrayOutputStream;
  52. import java.io.File;
  53. import java.io.FileOutputStream;
  54. import java.io.IOException;
  55. import java.io.InputStream;
  56. import java.io.OutputStream;
  57. import java.nio.file.Files;
  58. import java.util.ArrayList;
  59. import java.util.Arrays;
  60. import java.util.Collections;
  61. import java.util.Iterator;
  62. import java.util.List;
  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(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(), UTF_8);
  186. assertEquals("Public key has too many lines", 1, lines.size());
  187. String pubKey = lines.get(0);
  188. // Strip off the comment.
  189. String[] parts = pubKey.split("\\s+");
  190. assertTrue("Unexpected key content",
  191. parts.length == 2 || parts.length == 3);
  192. String keyPart = parts[0] + ' ' + parts[1];
  193. String line = '[' + host + "]:" + port + ' ' + keyPart;
  194. Files.write(file.toPath(), Collections.singletonList(line));
  195. return keyPart;
  196. }
  197. /**
  198. * Checks whether there is a line for the given host and port that also
  199. * matches the given key part in the list of lines.
  200. *
  201. * @param host
  202. * to look for
  203. * @param port
  204. * to look for
  205. * @param keyPart
  206. * to look for
  207. * @param lines
  208. * to look in
  209. * @return {@code true} if found, {@code false} otherwise
  210. */
  211. protected boolean hasHostKey(String host, int port, String keyPart,
  212. List<String> lines) {
  213. String h = '[' + host + "]:" + port;
  214. return lines.stream()
  215. .anyMatch(l -> l.contains(h) && l.contains(keyPart));
  216. }
  217. @After
  218. public void shutdownServer() throws Exception {
  219. if (server != null) {
  220. server.stop();
  221. server = null;
  222. }
  223. FS.DETECTED.setUserHome(homeDir);
  224. SshSessionFactory.setInstance(null);
  225. factory = null;
  226. }
  227. protected abstract SshSessionFactory createSessionFactory();
  228. protected SshSessionFactory getSessionFactory() {
  229. return factory;
  230. }
  231. protected abstract void installConfig(String... config);
  232. /**
  233. * Copies a test data file contained in the test bundle to the given file.
  234. * Equivalent to {@link #copyTestResource(Class, String, File)} with
  235. * {@code SshTestHarness.class} as first parameter.
  236. *
  237. * @param resourceName
  238. * of the test resource to copy
  239. * @param to
  240. * file to copy the resource to
  241. * @throws IOException
  242. * if the resource cannot be copied
  243. */
  244. protected void copyTestResource(String resourceName, File to)
  245. throws IOException {
  246. copyTestResource(SshTestHarness.class, resourceName, to);
  247. }
  248. /**
  249. * Copies a test data file contained in the test bundle to the given file,
  250. * using {@link Class#getResourceAsStream(String)} to get the test resource.
  251. *
  252. * @param loader
  253. * {@link Class} to use to load the resource
  254. * @param resourceName
  255. * of the test resource to copy
  256. * @param to
  257. * file to copy the resource to
  258. * @throws IOException
  259. * if the resource cannot be copied
  260. */
  261. protected void copyTestResource(Class<?> loader, String resourceName,
  262. File to) throws IOException {
  263. try (InputStream in = loader.getResourceAsStream(resourceName)) {
  264. Files.copy(in, to.toPath());
  265. }
  266. }
  267. protected File cloneWith(String uri, File to, CredentialsProvider provider,
  268. String... config) throws Exception {
  269. installConfig(config);
  270. CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true)
  271. .setDirectory(to).setURI(uri);
  272. if (provider != null) {
  273. clone.setCredentialsProvider(provider);
  274. }
  275. try (Git git = clone.call()) {
  276. Repository repo = git.getRepository();
  277. assertNotNull(repo.resolve("master"));
  278. assertNotEquals(db.getWorkTree(),
  279. git.getRepository().getWorkTree());
  280. assertTrue(new File(git.getRepository().getWorkTree(), "file.txt")
  281. .exists());
  282. return repo.getWorkTree();
  283. }
  284. }
  285. protected void pushTo(File localClone) throws Exception {
  286. pushTo(null, localClone);
  287. }
  288. protected void pushTo(CredentialsProvider provider, File localClone)
  289. throws Exception {
  290. RevCommit commit;
  291. File newFile = null;
  292. try (Git git = Git.open(localClone)) {
  293. // Write a new file and modify a file.
  294. Repository local = git.getRepository();
  295. newFile = File.createTempFile("new", "sshtest",
  296. local.getWorkTree());
  297. write(newFile, "something new");
  298. File existingFile = new File(local.getWorkTree(), "file.txt");
  299. write(existingFile, "something else");
  300. git.add().addFilepattern("file.txt")
  301. .addFilepattern(newFile.getName())
  302. .call();
  303. commit = git.commit().setMessage("Local commit").call();
  304. // Push
  305. PushCommand push = git.push().setPushAll();
  306. if (provider != null) {
  307. push.setCredentialsProvider(provider);
  308. }
  309. Iterable<PushResult> results = push.call();
  310. for (PushResult result : results) {
  311. for (RemoteRefUpdate u : result.getRemoteUpdates()) {
  312. assertEquals(
  313. "Could not update " + u.getRemoteName() + ' '
  314. + u.getMessage(),
  315. RemoteRefUpdate.Status.OK, u.getStatus());
  316. }
  317. }
  318. }
  319. // Now check "master" in the remote repo directly:
  320. assertEquals("Unexpected remote commit", commit, db.resolve("master"));
  321. assertEquals("Unexpected remote commit", commit,
  322. db.resolve(Constants.HEAD));
  323. File remoteFile = new File(db.getWorkTree(), newFile.getName());
  324. assertFalse("File should not exist on remote", remoteFile.exists());
  325. try (Git git = new Git(db)) {
  326. git.reset().setMode(ResetType.HARD).setRef(Constants.HEAD).call();
  327. }
  328. assertTrue("File does not exist on remote", remoteFile.exists());
  329. checkFile(remoteFile, "something new");
  330. }
  331. protected static class TestCredentialsProvider extends CredentialsProvider {
  332. private final List<String> stringStore;
  333. private final Iterator<String> strings;
  334. public TestCredentialsProvider(String... strings) {
  335. if (strings == null || strings.length == 0) {
  336. stringStore = Collections.emptyList();
  337. } else {
  338. stringStore = Arrays.asList(strings);
  339. }
  340. this.strings = stringStore.iterator();
  341. }
  342. @Override
  343. public boolean isInteractive() {
  344. return true;
  345. }
  346. @Override
  347. public boolean supports(CredentialItem... items) {
  348. return true;
  349. }
  350. @Override
  351. public boolean get(URIish uri, CredentialItem... items)
  352. throws UnsupportedCredentialItem {
  353. System.out.println("URI: " + uri);
  354. for (CredentialItem item : items) {
  355. System.out.println(item.getClass().getSimpleName() + ' '
  356. + item.getPromptText());
  357. }
  358. logItems(uri, items);
  359. for (CredentialItem item : items) {
  360. if (item instanceof CredentialItem.InformationalMessage) {
  361. continue;
  362. }
  363. if (item instanceof CredentialItem.YesNoType) {
  364. ((CredentialItem.YesNoType) item).setValue(true);
  365. } else if (item instanceof CredentialItem.CharArrayType) {
  366. if (strings.hasNext()) {
  367. ((CredentialItem.CharArrayType) item)
  368. .setValue(strings.next().toCharArray());
  369. } else {
  370. return false;
  371. }
  372. } else if (item instanceof CredentialItem.StringType) {
  373. if (strings.hasNext()) {
  374. ((CredentialItem.StringType) item)
  375. .setValue(strings.next());
  376. } else {
  377. return false;
  378. }
  379. } else {
  380. return false;
  381. }
  382. }
  383. return true;
  384. }
  385. private List<LogEntry> log = new ArrayList<>();
  386. private void logItems(URIish uri, CredentialItem... items) {
  387. log.add(new LogEntry(uri, Arrays.asList(items)));
  388. }
  389. public List<LogEntry> getLog() {
  390. return log;
  391. }
  392. }
  393. protected static class LogEntry {
  394. private URIish uri;
  395. private List<CredentialItem> items;
  396. public LogEntry(URIish uri, List<CredentialItem> items) {
  397. this.uri = uri;
  398. this.items = items;
  399. }
  400. public URIish getURIish() {
  401. return uri;
  402. }
  403. public List<CredentialItem> getItems() {
  404. return items;
  405. }
  406. }
  407. }