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