1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.transport.sshd;
11
12 import static org.apache.sshd.core.CoreModuleProperties.MAX_CONCURRENT_SESSIONS;
13 import static org.junit.Assert.assertEquals;
14 import static org.junit.Assert.assertFalse;
15 import static org.junit.Assert.assertNotNull;
16 import static org.junit.Assert.assertThrows;
17 import static org.junit.Assert.assertTrue;
18
19 import java.io.BufferedWriter;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.UncheckedIOException;
23 import java.net.URISyntaxException;
24 import java.nio.charset.StandardCharsets;
25 import java.nio.file.Files;
26 import java.nio.file.StandardOpenOption;
27 import java.security.KeyPair;
28 import java.security.KeyPairGenerator;
29 import java.security.PublicKey;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.stream.Collectors;
34
35 import org.apache.sshd.client.config.hosts.KnownHostEntry;
36 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
37 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
38 import org.apache.sshd.common.config.keys.KeyUtils;
39 import org.apache.sshd.common.config.keys.PublicKeyEntry;
40 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
41 import org.apache.sshd.common.session.Session;
42 import org.apache.sshd.common.util.net.SshdSocketAddress;
43 import org.apache.sshd.server.ServerAuthenticationManager;
44 import org.apache.sshd.server.SshServer;
45 import org.apache.sshd.server.forward.StaticDecisionForwardingFilter;
46 import org.eclipse.jgit.api.Git;
47 import org.eclipse.jgit.api.errors.TransportException;
48 import org.eclipse.jgit.junit.ssh.SshTestBase;
49 import org.eclipse.jgit.lib.Constants;
50 import org.eclipse.jgit.transport.RemoteSession;
51 import org.eclipse.jgit.transport.SshSessionFactory;
52 import org.eclipse.jgit.transport.URIish;
53 import org.eclipse.jgit.util.FS;
54 import org.junit.Test;
55 import org.junit.experimental.theories.Theories;
56 import org.junit.runner.RunWith;
57
58 @RunWith(Theories.class)
59 public class ApacheSshTest extends SshTestBase {
60
61 @Override
62 protected SshSessionFactory createSessionFactory() {
63 SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(),
64 null);
65
66 result.setHomeDirectory(FS.DETECTED.userHome());
67 result.setSshDirectory(sshDir);
68 return result;
69 }
70
71 @Override
72 protected void installConfig(String... config) {
73 File configFile = new File(sshDir, Constants.CONFIG);
74 if (config != null) {
75 try {
76 Files.write(configFile.toPath(), Arrays.asList(config));
77 } catch (IOException e) {
78 throw new UncheckedIOException(e);
79 }
80 }
81 }
82
83 @Test
84 public void testEd25519HostKey() throws Exception {
85
86
87 File newHostKey = new File(getTemporaryDirectory(), "newhostkey");
88 copyTestResource("id_ed25519", newHostKey);
89 server.addHostKey(newHostKey.toPath(), true);
90 File newHostKeyPub = new File(getTemporaryDirectory(),
91 "newhostkey.pub");
92 copyTestResource("id_ed25519.pub", newHostKeyPub);
93 createKnownHostsFile(knownHosts, "localhost", testPort, newHostKeyPub);
94 cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
95 "Host git",
96 "HostName localhost",
97 "Port " + testPort,
98 "User " + TEST_USER,
99 "IdentityFile " + privateKey1.getAbsolutePath());
100 }
101
102 @Test
103 public void testHashedKnownHosts() throws Exception {
104 assertTrue("Failed to delete known_hosts", knownHosts.delete());
105
106
107 TestCredentialsProvider provider = new TestCredentialsProvider();
108 cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
109 "HashKnownHosts yes",
110 "Host localhost",
111 "HostName localhost",
112 "Port " + testPort,
113 "User " + TEST_USER,
114 "IdentityFile " + privateKey1.getAbsolutePath());
115 List<LogEntry> messages = provider.getLog();
116 assertFalse("Expected user interaction", messages.isEmpty());
117 assertEquals(
118 "Expected to be asked about the key, and the file creation", 2,
119 messages.size());
120 assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists());
121
122
123 File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
124 cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
125 "Host localhost",
126 "HostName localhost",
127 "Port " + testPort,
128 "User " + TEST_USER,
129 "IdentityFile " + privateKey1.getAbsolutePath());
130
131
132 List<String> lines = Files.readAllLines(knownHosts.toPath()).stream()
133 .filter(s -> s != null && s.length() >= 1 && s.charAt(0) != '#'
134 && !s.trim().isEmpty())
135 .collect(Collectors.toList());
136 assertEquals("Unexpected number of known_hosts lines", 1, lines.size());
137 String line = lines.get(0);
138 assertFalse("Found host in line", line.contains("localhost"));
139 assertFalse("Found IP in line", line.contains("127.0.0.1"));
140 assertTrue("Hash not found", line.contains("|"));
141 KnownHostEntry entry = KnownHostEntry.parseKnownHostEntry(line);
142 assertTrue("Hash doesn't match localhost",
143 entry.isHostMatch("localhost", testPort)
144 || entry.isHostMatch("127.0.0.1", testPort));
145 }
146
147 @Test
148 public void testPreamble() throws Exception {
149
150
151 StringBuilder b = new StringBuilder();
152 for (int i = 0; i < 257; i++) {
153 b.append('a');
154 }
155 server.setPreamble("A line with a \000 NUL",
156 "A long line: " + b.toString());
157 cloneWith(
158 "ssh://" + TEST_USER + "@localhost:" + testPort
159 + "/doesntmatter",
160 defaultCloneDir, null,
161 "IdentityFile " + privateKey1.getAbsolutePath());
162 }
163
164 @Test
165 public void testLongPreamble() throws Exception {
166
167 StringBuilder b = new StringBuilder();
168 for (int i = 0; i < 1024; i++) {
169 b.append('a');
170 }
171 String line = b.toString();
172 String[] lines = new String[60];
173 for (int i = 0; i < lines.length; i++) {
174 lines[i] = line;
175 }
176 server.setPreamble(lines);
177 cloneWith(
178 "ssh://" + TEST_USER + "@localhost:" + testPort
179 + "/doesntmatter",
180 defaultCloneDir, null,
181 "IdentityFile " + privateKey1.getAbsolutePath());
182 }
183
184 @Test
185 public void testHugePreamble() throws Exception {
186
187 StringBuilder b = new StringBuilder();
188 for (int i = 0; i < 1024; i++) {
189 b.append('a');
190 }
191 String line = b.toString();
192 String[] lines = new String[70];
193 for (int i = 0; i < lines.length; i++) {
194 lines[i] = line;
195 }
196 server.setPreamble(lines);
197 TransportException e = assertThrows(TransportException.class,
198 () -> cloneWith(
199 "ssh://" + TEST_USER + "@localhost:" + testPort
200 + "/doesntmatter",
201 defaultCloneDir, null,
202 "IdentityFile " + privateKey1.getAbsolutePath()));
203
204 assertFalse(e.getMessage().contains("timeout"));
205 assertTrue(e.getMessage().contains("65536")
206 || e.getMessage().contains("closed"));
207 }
208
209
210
211
212
213
214
215
216
217
218 @Test
219 public void testCloneAndFetchWithSessionLimit() throws Exception {
220 MAX_CONCURRENT_SESSIONS
221 .set(server.getPropertyResolver(), Integer.valueOf(2));
222 File localClone = cloneWith("ssh://localhost/doesntmatter",
223 defaultCloneDir, null,
224 "Host localhost",
225 "HostName localhost",
226 "Port " + testPort,
227 "User " + TEST_USER,
228 "IdentityFile " + privateKey1.getAbsolutePath());
229
230 try (Git git = Git.open(localClone)) {
231 git.fetch().call();
232 git.fetch().call();
233 }
234 }
235
236
237
238
239
240
241
242
243
244
245
246 private SshServer createServer(String user, File userKey) throws Exception {
247 SshServer srv = SshServer.setUpDefaultServer();
248
249 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
250 generator.initialize(2048);
251 KeyPair proxyHostKey = generator.generateKeyPair();
252 srv.setKeyPairProvider(
253 session -> Collections.singletonList(proxyHostKey));
254
255 srv.setUserAuthFactories(Collections.singletonList(
256 ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY));
257
258 PublicKey userProxyKey = AuthorizedKeyEntry
259 .readAuthorizedKeys(userKey.toPath()).get(0)
260 .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
261 srv.setPublickeyAuthenticator(
262 (userName, publicKey, session) -> user.equals(userName)
263 && KeyUtils.compareKeys(userProxyKey, publicKey));
264 return srv;
265 }
266
267
268
269
270
271
272
273 private void registerServer(SshServer srv) throws Exception {
274
275 try (BufferedWriter writer = Files.newBufferedWriter(
276 knownHosts.toPath(), StandardCharsets.US_ASCII,
277 StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
278 writer.append('\n');
279 KnownHostHashValue.appendHostPattern(writer, "localhost",
280 srv.getPort());
281 writer.append(',');
282 KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
283 srv.getPort());
284 writer.append(' ');
285 PublicKeyEntry.appendPublicKeyEntry(writer,
286 srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic());
287 writer.append('\n');
288 }
289 }
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305 private SshServer createProxy(String user, File userKey,
306 SshdSocketAddress[] report) throws Exception {
307 SshServer proxy = createServer(user, userKey);
308
309 proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) {
310
311 @Override
312 protected boolean checkAcceptance(String request, Session session,
313 SshdSocketAddress target) {
314 report[0] = target;
315 return super.checkAcceptance(request, session, target);
316 }
317 });
318 proxy.start();
319 registerServer(proxy);
320 return proxy;
321 }
322
323 @Test
324 public void testJumpHost() throws Exception {
325 SshdSocketAddress[] forwarded = { null };
326 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
327 forwarded)) {
328 try {
329
330 cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
331 "Host server",
332 "HostName localhost",
333 "Port " + testPort,
334 "User " + TEST_USER,
335 "IdentityFile " + privateKey1.getAbsolutePath(),
336 "ProxyJump " + TEST_USER + "X@proxy:" + proxy.getPort(),
337 "",
338 "Host proxy",
339 "Hostname localhost",
340 "IdentityFile " + privateKey2.getAbsolutePath());
341 assertNotNull(forwarded[0]);
342 assertEquals(testPort, forwarded[0].getPort());
343 } finally {
344 proxy.stop();
345 }
346 }
347 }
348
349 @Test
350 public void testJumpHostWrongKeyAtProxy() throws Exception {
351
352 SshdSocketAddress[] forwarded = { null };
353 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
354 forwarded)) {
355 try {
356
357 TransportException e = assertThrows(TransportException.class,
358 () -> cloneWith("ssh://server/doesntmatter",
359 defaultCloneDir, null,
360 "Host server",
361 "HostName localhost",
362 "Port " + testPort,
363 "User " + TEST_USER,
364 "IdentityFile " + privateKey1.getAbsolutePath(),
365 "ProxyJump " + TEST_USER + "X@proxy:"
366 + proxy.getPort(),
367 "",
368 "Host proxy",
369 "Hostname localhost",
370 "IdentityFile "
371 + privateKey1.getAbsolutePath()));
372 String message = e.getMessage();
373 assertTrue(message.contains("localhost:" + proxy.getPort()));
374 assertTrue(message.contains("proxy:" + proxy.getPort()));
375 } finally {
376 proxy.stop();
377 }
378 }
379 }
380
381 @Test
382 public void testJumpHostWrongKeyAtServer() throws Exception {
383
384 SshdSocketAddress[] forwarded = { null };
385 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
386 forwarded)) {
387 try {
388
389 TransportException e = assertThrows(TransportException.class,
390 () -> cloneWith("ssh://server/doesntmatter",
391 defaultCloneDir, null,
392 "Host server",
393 "HostName localhost",
394 "Port " + testPort,
395 "User " + TEST_USER,
396 "IdentityFile " + privateKey2.getAbsolutePath(),
397 "ProxyJump " + TEST_USER + "X@proxy:"
398 + proxy.getPort(),
399 "",
400 "Host proxy",
401 "Hostname localhost",
402 "IdentityFile "
403 + privateKey2.getAbsolutePath()));
404 String message = e.getMessage();
405 assertTrue(message.contains("localhost:" + testPort));
406 assertTrue(message.contains("ssh://server"));
407 } finally {
408 proxy.stop();
409 }
410 }
411 }
412
413 @Test
414 public void testJumpHostNonSsh() throws Exception {
415 SshdSocketAddress[] forwarded = { null };
416 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
417 forwarded)) {
418 try {
419 TransportException e = assertThrows(TransportException.class,
420 () -> cloneWith("ssh://server/doesntmatter",
421 defaultCloneDir, null,
422 "Host server",
423 "HostName localhost",
424 "Port " + testPort,
425 "User " + TEST_USER,
426 "IdentityFile " + privateKey1.getAbsolutePath(),
427 "ProxyJump http://" + TEST_USER + "X@proxy:"
428 + proxy.getPort(),
429 "",
430 "Host proxy",
431 "Hostname localhost",
432 "IdentityFile "
433 + privateKey2.getAbsolutePath()));
434
435 Throwable t = e;
436 while (t != null) {
437 if (t instanceof URISyntaxException) {
438 break;
439 }
440 t = t.getCause();
441 }
442 assertNotNull(t);
443 assertTrue(t.getMessage().contains("Non-ssh"));
444 } finally {
445 proxy.stop();
446 }
447 }
448 }
449
450 @Test
451 public void testJumpHostWithPath() throws Exception {
452 SshdSocketAddress[] forwarded = { null };
453 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
454 forwarded)) {
455 try {
456 TransportException e = assertThrows(TransportException.class,
457 () -> cloneWith("ssh://server/doesntmatter",
458 defaultCloneDir, null,
459 "Host server",
460 "HostName localhost",
461 "Port " + testPort,
462 "User " + TEST_USER,
463 "IdentityFile " + privateKey1.getAbsolutePath(),
464 "ProxyJump ssh://" + TEST_USER + "X@proxy:"
465 + proxy.getPort() + "/wrongPath",
466 "",
467 "Host proxy",
468 "Hostname localhost",
469 "IdentityFile "
470 + privateKey2.getAbsolutePath()));
471
472 Throwable t = e;
473 while (t != null) {
474 if (t instanceof URISyntaxException) {
475 break;
476 }
477 t = t.getCause();
478 }
479 assertNotNull(t);
480 assertTrue(t.getMessage().contains("wrongPath"));
481 } finally {
482 proxy.stop();
483 }
484 }
485 }
486
487 @Test
488 public void testJumpHostWithPathShort() throws Exception {
489 SshdSocketAddress[] forwarded = { null };
490 try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
491 forwarded)) {
492 try {
493 TransportException e = assertThrows(TransportException.class,
494 () -> cloneWith("ssh://server/doesntmatter",
495 defaultCloneDir, null,
496 "Host server",
497 "HostName localhost",
498 "Port " + testPort,
499 "User " + TEST_USER,
500 "IdentityFile " + privateKey1.getAbsolutePath(),
501 "ProxyJump " + TEST_USER + "X@proxy:wrongPath",
502 "",
503 "Host proxy",
504 "Hostname localhost",
505 "Port " + proxy.getPort(),
506 "IdentityFile "
507 + privateKey2.getAbsolutePath()));
508
509 Throwable t = e;
510 while (t != null) {
511 if (t instanceof URISyntaxException) {
512 break;
513 }
514 t = t.getCause();
515 }
516 assertNotNull(t);
517 assertTrue(t.getMessage().contains("wrongPath"));
518 } finally {
519 proxy.stop();
520 }
521 }
522 }
523
524 @Test
525 public void testJumpHostChain() throws Exception {
526 SshdSocketAddress[] forwarded1 = { null };
527 SshdSocketAddress[] forwarded2 = { null };
528 try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
529 forwarded1);
530 SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
531 try {
532
533 cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
534 "Host server",
535 "HostName localhost",
536 "Port " + testPort,
537 "User " + TEST_USER,
538 "IdentityFile " + privateKey1.getAbsolutePath(),
539 "ProxyJump proxy2," + TEST_USER + "X@proxy:"
540 + proxy1.getPort(),
541 "",
542 "Host proxy",
543 "Hostname localhost",
544 "IdentityFile " + privateKey2.getAbsolutePath(),
545 "",
546 "Host proxy2",
547 "Hostname localhost",
548 "User foo",
549 "Port " + proxy2.getPort(),
550 "IdentityFile " + privateKey1.getAbsolutePath());
551 assertNotNull(forwarded1[0]);
552 assertEquals(proxy2.getPort(), forwarded1[0].getPort());
553 assertNotNull(forwarded2[0]);
554 assertEquals(testPort, forwarded2[0].getPort());
555 } finally {
556 proxy1.stop();
557 proxy2.stop();
558 }
559 }
560 }
561
562 @Test
563 public void testJumpHostCascade() throws Exception {
564 SshdSocketAddress[] forwarded1 = { null };
565 SshdSocketAddress[] forwarded2 = { null };
566 try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
567 forwarded1);
568 SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
569 try {
570
571 cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
572 "Host server",
573 "HostName localhost",
574 "Port " + testPort,
575 "User " + TEST_USER,
576 "IdentityFile " + privateKey1.getAbsolutePath(),
577 "ProxyJump " + TEST_USER + "X@proxy",
578 "",
579 "Host proxy",
580 "Hostname localhost",
581 "Port " + proxy1.getPort(),
582 "ProxyJump ssh://proxy2:" + proxy2.getPort(), //
583 "IdentityFile " + privateKey2.getAbsolutePath(),
584 "",
585 "Host proxy2",
586 "Hostname localhost",
587 "User foo",
588 "IdentityFile " + privateKey1.getAbsolutePath());
589 assertNotNull(forwarded1[0]);
590 assertEquals(testPort, forwarded1[0].getPort());
591 assertNotNull(forwarded2[0]);
592 assertEquals(proxy1.getPort(), forwarded2[0].getPort());
593 } finally {
594 proxy1.stop();
595 proxy2.stop();
596 }
597 }
598 }
599
600 @Test
601 public void testJumpHostRecursion() throws Exception {
602 SshdSocketAddress[] forwarded1 = { null };
603 SshdSocketAddress[] forwarded2 = { null };
604 try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
605 forwarded1);
606 SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
607 try {
608 TransportException e = assertThrows(TransportException.class,
609 () -> cloneWith(
610 "ssh://server/doesntmatter", defaultCloneDir, null, //
611 "Host server",
612 "HostName localhost",
613 "Port " + testPort,
614 "User " + TEST_USER,
615 "IdentityFile " + privateKey1.getAbsolutePath(),
616 "ProxyJump " + TEST_USER + "X@proxy",
617 "",
618 "Host proxy",
619 "Hostname localhost",
620 "Port " + proxy1.getPort(),
621 "ProxyJump ssh://proxy2:" + proxy2.getPort(), //
622 "IdentityFile " + privateKey2.getAbsolutePath(),
623 "",
624 "Host proxy2",
625 "Hostname localhost",
626 "User foo",
627 "ProxyJump " + TEST_USER + "X@proxy",
628 "IdentityFile " + privateKey1.getAbsolutePath()));
629 assertTrue(e.getMessage().contains("proxy"));
630 } finally {
631 proxy1.stop();
632 proxy2.stop();
633 }
634 }
635 }
636
637
638
639
640
641
642
643
644
645
646
647 @Test
648 public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms()
649 throws Exception {
650 try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
651 oldServer.setSignatureFactoriesNames("ssh-rsa");
652 oldServer.start();
653 registerServer(oldServer);
654 installConfig("Host server",
655 "HostName localhost",
656 "Port " + oldServer.getPort(),
657 "User " + TEST_USER,
658 "IdentityFile " + privateKey1.getAbsolutePath(),
659 "PubkeyAcceptedAlgorithms ^ssh-rsa");
660 RemoteSession session = getSessionFactory().getSession(
661 new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
662 10000);
663 assertNotNull(session);
664 session.disconnect();
665 }
666 }
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687 @Test
688 public void testConnectAuthSshRsa() throws Exception {
689 try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
690 oldServer.setSignatureFactoriesNames("ssh-rsa");
691 oldServer.start();
692 registerServer(oldServer);
693 installConfig("Host server",
694 "HostName localhost",
695 "Port " + oldServer.getPort(),
696 "User " + TEST_USER,
697 "IdentityFile " + privateKey1.getAbsolutePath());
698 RemoteSession session = getSessionFactory().getSession(
699 new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
700 10000);
701 assertNotNull(session);
702 session.disconnect();
703 }
704 }
705 }