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