1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 package org.eclipse.jgit.transport.ssh;
44
45 import static java.nio.charset.StandardCharsets.US_ASCII;
46 import static java.nio.charset.StandardCharsets.UTF_8;
47 import static org.junit.Assert.assertEquals;
48 import static org.junit.Assert.assertFalse;
49 import static org.junit.Assert.assertNotEquals;
50 import static org.junit.Assert.assertNotNull;
51 import static org.junit.Assert.assertTrue;
52
53 import java.io.ByteArrayOutputStream;
54 import java.io.File;
55 import java.io.FileOutputStream;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.io.OutputStream;
59 import java.nio.file.Files;
60 import java.util.ArrayList;
61 import java.util.Arrays;
62 import java.util.Collections;
63 import java.util.Iterator;
64 import java.util.List;
65
66 import org.eclipse.jgit.api.CloneCommand;
67 import org.eclipse.jgit.api.Git;
68 import org.eclipse.jgit.api.PushCommand;
69 import org.eclipse.jgit.api.ResetCommand.ResetType;
70 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
71 import org.eclipse.jgit.junit.RepositoryTestCase;
72 import org.eclipse.jgit.junit.ssh.SshTestGitServer;
73 import org.eclipse.jgit.lib.Constants;
74 import org.eclipse.jgit.lib.Repository;
75 import org.eclipse.jgit.revwalk.RevCommit;
76 import org.eclipse.jgit.transport.CredentialItem;
77 import org.eclipse.jgit.transport.CredentialsProvider;
78 import org.eclipse.jgit.transport.PushResult;
79 import org.eclipse.jgit.transport.RemoteRefUpdate;
80 import org.eclipse.jgit.transport.SshSessionFactory;
81 import org.eclipse.jgit.transport.URIish;
82 import org.eclipse.jgit.util.FS;
83 import org.junit.After;
84
85 import com.jcraft.jsch.JSch;
86 import com.jcraft.jsch.KeyPair;
87
88
89
90
91
92
93
94
95
96
97
98
99
100 public abstract class SshTestHarness extends RepositoryTestCase {
101
102 protected static final String TEST_USER = "testuser";
103
104 protected File sshDir;
105
106 protected File privateKey1;
107
108 protected File privateKey2;
109
110 protected File publicKey1;
111
112 protected SshTestGitServer server;
113
114 private SshSessionFactory factory;
115
116 protected int testPort;
117
118 protected File knownHosts;
119
120 private File homeDir;
121
122 @Override
123 public void setUp() throws Exception {
124 super.setUp();
125 writeTrashFile("file.txt", "something");
126 try (Git git = new Git(db)) {
127 git.add().addFilepattern("file.txt").call();
128 git.commit().setMessage("Initial commit").call();
129 }
130 mockSystemReader.setProperty("user.home",
131 getTemporaryDirectory().getAbsolutePath());
132 mockSystemReader.setProperty("HOME",
133 getTemporaryDirectory().getAbsolutePath());
134 homeDir = FS.DETECTED.userHome();
135 FS.DETECTED.setUserHome(getTemporaryDirectory().getAbsoluteFile());
136 sshDir = new File(getTemporaryDirectory(), ".ssh");
137 assertTrue(sshDir.mkdir());
138 File serverDir = new File(getTemporaryDirectory(), "srv");
139 assertTrue(serverDir.mkdir());
140
141 privateKey1 = new File(sshDir, "first_key");
142 privateKey2 = new File(sshDir, "second_key");
143 publicKey1 = createKeyPair(privateKey1);
144 createKeyPair(privateKey2);
145 ByteArrayOutputStream publicHostKey = new ByteArrayOutputStream();
146
147 server = new SshTestGitServer(TEST_USER, publicKey1.toPath(), db,
148 createHostKey(publicHostKey));
149 testPort = server.start();
150 assertTrue(testPort > 0);
151 knownHosts = new File(sshDir, "known_hosts");
152 Files.write(knownHosts.toPath(), Collections.singleton("[localhost]:"
153 + testPort + ' '
154 + publicHostKey.toString(US_ASCII.name())));
155 factory = createSessionFactory();
156 SshSessionFactory.setInstance(factory);
157 }
158
159 private static File createKeyPair(File privateKeyFile) throws Exception {
160
161
162 JSch jsch = new JSch();
163 KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
164 try (OutputStream out = new FileOutputStream(privateKeyFile)) {
165 pair.writePrivateKey(out);
166 }
167 File publicKeyFile = new File(privateKeyFile.getParentFile(),
168 privateKeyFile.getName() + ".pub");
169 try (OutputStream out = new FileOutputStream(publicKeyFile)) {
170 pair.writePublicKey(out, TEST_USER);
171 }
172 return publicKeyFile;
173 }
174
175 private static byte[] createHostKey(OutputStream publicKey)
176 throws Exception {
177 JSch jsch = new JSch();
178 KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
179 pair.writePublicKey(publicKey, "");
180 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
181 pair.writePrivateKey(out);
182 out.flush();
183 return out.toByteArray();
184 }
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202 protected static String createKnownHostsFile(File file, String host,
203 int port, File publicKey) throws IOException {
204 List<String> lines = Files.readAllLines(publicKey.toPath(), UTF_8);
205 assertEquals("Public key has too many lines", 1, lines.size());
206 String pubKey = lines.get(0);
207
208 String[] parts = pubKey.split("\\s+");
209 assertTrue("Unexpected key content",
210 parts.length == 2 || parts.length == 3);
211 String keyPart = parts[0] + ' ' + parts[1];
212 String line = '[' + host + "]:" + port + ' ' + keyPart;
213 Files.write(file.toPath(), Collections.singletonList(line));
214 return keyPart;
215 }
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231 protected boolean hasHostKey(String host, int port, String keyPart,
232 List<String> lines) {
233 String h = '[' + host + "]:" + port;
234 return lines.stream()
235 .anyMatch(l -> l.contains(h) && l.contains(keyPart));
236 }
237
238 @After
239 public void shutdownServer() throws Exception {
240 if (server != null) {
241 server.stop();
242 server = null;
243 }
244 FS.DETECTED.setUserHome(homeDir);
245 SshSessionFactory.setInstance(null);
246 factory = null;
247 }
248
249 protected abstract SshSessionFactory createSessionFactory();
250
251 protected SshSessionFactory getSessionFactory() {
252 return factory;
253 }
254
255 protected abstract void installConfig(String... config);
256
257
258
259
260
261
262
263
264
265
266
267
268
269 protected void copyTestResource(String resourceName, File to)
270 throws IOException {
271 copyTestResource(SshTestHarness.class, resourceName, to);
272 }
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287 protected void copyTestResource(Class<?> loader, String resourceName,
288 File to) throws IOException {
289 try (InputStream in = loader.getResourceAsStream(resourceName)) {
290 Files.copy(in, to.toPath());
291 }
292 }
293
294 protected File cloneWith(String uri, File to, CredentialsProvider provider,
295 String... config) throws Exception {
296 installConfig(config);
297 CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true)
298 .setDirectory(to).setURI(uri);
299 if (provider != null) {
300 clone.setCredentialsProvider(provider);
301 }
302 try (Git git = clone.call()) {
303 Repository repo = git.getRepository();
304 assertNotNull(repo.resolve("master"));
305 assertNotEquals(db.getWorkTree(),
306 git.getRepository().getWorkTree());
307 assertTrue(new File(git.getRepository().getWorkTree(), "file.txt")
308 .exists());
309 return repo.getWorkTree();
310 }
311 }
312
313 protected void pushTo(File localClone) throws Exception {
314 pushTo(null, localClone);
315 }
316
317 protected void pushTo(CredentialsProvider provider, File localClone)
318 throws Exception {
319 RevCommit commit;
320 File newFile = null;
321 try (Git git = Git.open(localClone)) {
322
323 Repository local = git.getRepository();
324 newFile = File.createTempFile("new", "sshtest",
325 local.getWorkTree());
326 write(newFile, "something new");
327 File existingFile = new File(local.getWorkTree(), "file.txt");
328 write(existingFile, "something else");
329 git.add().addFilepattern("file.txt")
330 .addFilepattern(newFile.getName())
331 .call();
332 commit = git.commit().setMessage("Local commit").call();
333
334 PushCommand push = git.push().setPushAll();
335 if (provider != null) {
336 push.setCredentialsProvider(provider);
337 }
338 Iterable<PushResult> results = push.call();
339 for (PushResult result : results) {
340 for (RemoteRefUpdate u : result.getRemoteUpdates()) {
341 assertEquals(
342 "Could not update " + u.getRemoteName() + ' '
343 + u.getMessage(),
344 RemoteRefUpdate.Status.OK, u.getStatus());
345 }
346 }
347 }
348
349 assertEquals("Unexpected remote commit", commit, db.resolve("master"));
350 assertEquals("Unexpected remote commit", commit,
351 db.resolve(Constants.HEAD));
352 File remoteFile = new File(db.getWorkTree(), newFile.getName());
353 assertFalse("File should not exist on remote", remoteFile.exists());
354 try (Git git = new Git(db)) {
355 git.reset().setMode(ResetType.HARD).setRef(Constants.HEAD).call();
356 }
357 assertTrue("File does not exist on remote", remoteFile.exists());
358 checkFile(remoteFile, "something new");
359 }
360
361 protected static class TestCredentialsProvider extends CredentialsProvider {
362
363 private final List<String> stringStore;
364
365 private final Iterator<String> strings;
366
367 public TestCredentialsProvider(String... strings) {
368 if (strings == null || strings.length == 0) {
369 stringStore = Collections.emptyList();
370 } else {
371 stringStore = Arrays.asList(strings);
372 }
373 this.strings = stringStore.iterator();
374 }
375
376 @Override
377 public boolean isInteractive() {
378 return true;
379 }
380
381 @Override
382 public boolean supports(CredentialItem... items) {
383 return true;
384 }
385
386 @Override
387 public boolean get(URIish uri, CredentialItem... items)
388 throws UnsupportedCredentialItem {
389 System.out.println("URI: " + uri);
390 for (CredentialItem item : items) {
391 System.out.println(item.getClass().getSimpleName() + ' '
392 + item.getPromptText());
393 }
394 logItems(uri, items);
395 for (CredentialItem item : items) {
396 if (item instanceof CredentialItem.InformationalMessage) {
397 continue;
398 }
399 if (item instanceof CredentialItem.YesNoType) {
400 ((CredentialItem.YesNoType) item).setValue(true);
401 } else if (item instanceof CredentialItem.CharArrayType) {
402 if (strings.hasNext()) {
403 ((CredentialItem.CharArrayType) item)
404 .setValue(strings.next().toCharArray());
405 } else {
406 return false;
407 }
408 } else if (item instanceof CredentialItem.StringType) {
409 if (strings.hasNext()) {
410 ((CredentialItem.StringType) item)
411 .setValue(strings.next());
412 } else {
413 return false;
414 }
415 } else {
416 return false;
417 }
418 }
419 return true;
420 }
421
422 private List<LogEntry> log = new ArrayList<>();
423
424 private void logItems(URIish uri, CredentialItem... items) {
425 log.add(new LogEntry(uri, Arrays.asList(items)));
426 }
427
428 public List<LogEntry> getLog() {
429 return log;
430 }
431 }
432
433 protected static class LogEntry {
434
435 private URIish uri;
436
437 private List<CredentialItem> items;
438
439 public LogEntry(URIish uri, List<CredentialItem> items) {
440 this.uri = uri;
441 this.items = items;
442 }
443
444 public URIish getURIish() {
445 return uri;
446 }
447
448 public List<CredentialItem> getItems() {
449 return items;
450 }
451 }
452 }