View Javadoc
1   /*
2    * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
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.NamedFactory;
38  import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
39  import org.apache.sshd.common.config.keys.KeyUtils;
40  import org.apache.sshd.common.config.keys.PublicKeyEntry;
41  import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
42  import org.apache.sshd.common.kex.BuiltinDHFactories;
43  import org.apache.sshd.common.kex.DHFactory;
44  import org.apache.sshd.common.kex.KeyExchangeFactory;
45  import org.apache.sshd.common.session.Session;
46  import org.apache.sshd.common.util.net.SshdSocketAddress;
47  import org.apache.sshd.server.ServerAuthenticationManager;
48  import org.apache.sshd.server.ServerBuilder;
49  import org.apache.sshd.server.SshServer;
50  import org.apache.sshd.server.forward.StaticDecisionForwardingFilter;
51  import org.eclipse.jgit.api.Git;
52  import org.eclipse.jgit.api.errors.TransportException;
53  import org.eclipse.jgit.junit.ssh.SshTestBase;
54  import org.eclipse.jgit.lib.Constants;
55  import org.eclipse.jgit.transport.RemoteSession;
56  import org.eclipse.jgit.transport.SshSessionFactory;
57  import org.eclipse.jgit.transport.URIish;
58  import org.eclipse.jgit.util.FS;
59  import org.junit.Test;
60  import org.junit.experimental.theories.Theories;
61  import org.junit.runner.RunWith;
62  
63  @RunWith(Theories.class)
64  public class ApacheSshTest extends SshTestBase {
65  
66  	@Override
67  	protected SshSessionFactory createSessionFactory() {
68  		return new SshdSessionFactoryBuilder()
69  				// No proxies in tests
70  				.setProxyDataFactory(null)
71  				// No ssh-agent in tests
72  				.setConnectorFactory(null)
73  				// The home directory is mocked at this point!
74  				.setHomeDirectory(FS.DETECTED.userHome())
75  				.setSshDirectory(sshDir)
76  				.build(new JGitKeyCache());
77  	}
78  
79  	@Override
80  	protected void installConfig(String... config) {
81  		File configFile = new File(sshDir, Constants.CONFIG);
82  		if (config != null) {
83  			try {
84  				Files.write(configFile.toPath(), Arrays.asList(config));
85  			} catch (IOException e) {
86  				throw new UncheckedIOException(e);
87  			}
88  		}
89  	}
90  
91  	@Test
92  	public void testEd25519HostKey() throws Exception {
93  		// Using ed25519 user identities is tested in the super class in
94  		// testSshKeys().
95  		File newHostKey = new File(getTemporaryDirectory(), "newhostkey");
96  		copyTestResource("id_ed25519", newHostKey);
97  		server.addHostKey(newHostKey.toPath(), true);
98  		File newHostKeyPub = new File(getTemporaryDirectory(),
99  				"newhostkey.pub");
100 		copyTestResource("id_ed25519.pub", newHostKeyPub);
101 		createKnownHostsFile(knownHosts, "localhost", testPort, newHostKeyPub);
102 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
103 				"Host git", //
104 				"HostName localhost", //
105 				"Port " + testPort, //
106 				"User " + TEST_USER, //
107 				"IdentityFile " + privateKey1.getAbsolutePath());
108 	}
109 
110 	@Test
111 	public void testHashedKnownHosts() throws Exception {
112 		assertTrue("Failed to delete known_hosts", knownHosts.delete());
113 		// The provider will answer "yes" to all questions, so we should be able
114 		// to connect and end up with a new known_hosts file with the host key.
115 		TestCredentialsProvider provider = new TestCredentialsProvider();
116 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
117 				"HashKnownHosts yes", //
118 				"Host localhost", //
119 				"HostName localhost", //
120 				"Port " + testPort, //
121 				"User " + TEST_USER, //
122 				"IdentityFile " + privateKey1.getAbsolutePath());
123 		List<LogEntry> messages = provider.getLog();
124 		assertFalse("Expected user interaction", messages.isEmpty());
125 		assertEquals(
126 				"Expected to be asked about the key, and the file creation", 2,
127 				messages.size());
128 		assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists());
129 		// Let's clone again without provider. If it works, the server host key
130 		// was written correctly.
131 		File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
132 		cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
133 				"Host localhost", //
134 				"HostName localhost", //
135 				"Port " + testPort, //
136 				"User " + TEST_USER, //
137 				"IdentityFile " + privateKey1.getAbsolutePath());
138 		// Check that the first line contains neither "localhost" nor
139 		// "127.0.0.1", but does contain the expected hash.
140 		List<String> lines = Files.readAllLines(knownHosts.toPath()).stream()
141 				.filter(s -> s != null && s.length() >= 1 && s.charAt(0) != '#'
142 						&& !s.trim().isEmpty())
143 				.collect(Collectors.toList());
144 		assertEquals("Unexpected number of known_hosts lines", 1, lines.size());
145 		String line = lines.get(0);
146 		assertFalse("Found host in line", line.contains("localhost"));
147 		assertFalse("Found IP in line", line.contains("127.0.0.1"));
148 		assertTrue("Hash not found", line.contains("|"));
149 		KnownHostEntry entry = KnownHostEntry.parseKnownHostEntry(line);
150 		assertTrue("Hash doesn't match localhost",
151 				entry.isHostMatch("localhost", testPort)
152 						|| entry.isHostMatch("127.0.0.1", testPort));
153 	}
154 
155 	@Test
156 	public void testPreamble() throws Exception {
157 		// Test that the client can deal with strange lines being sent before
158 		// the server identification string.
159 		StringBuilder b = new StringBuilder();
160 		for (int i = 0; i < 257; i++) {
161 			b.append('a');
162 		}
163 		server.setPreamble("A line with a \000 NUL",
164 				"A long line: " + b.toString());
165 		cloneWith(
166 				"ssh://" + TEST_USER + "@localhost:" + testPort
167 						+ "/doesntmatter",
168 				defaultCloneDir, null,
169 				"IdentityFile " + privateKey1.getAbsolutePath());
170 	}
171 
172 	@Test
173 	public void testLongPreamble() throws Exception {
174 		// Test that the client can deal with a long (about 60k) preamble.
175 		StringBuilder b = new StringBuilder();
176 		for (int i = 0; i < 1024; i++) {
177 			b.append('a');
178 		}
179 		String line = b.toString();
180 		String[] lines = new String[60];
181 		for (int i = 0; i < lines.length; i++) {
182 			lines[i] = line;
183 		}
184 		server.setPreamble(lines);
185 		cloneWith(
186 				"ssh://" + TEST_USER + "@localhost:" + testPort
187 						+ "/doesntmatter",
188 				defaultCloneDir, null,
189 				"IdentityFile " + privateKey1.getAbsolutePath());
190 	}
191 
192 	@Test
193 	public void testHugePreamble() throws Exception {
194 		// Test that the connection fails when the preamble is longer than 64k.
195 		StringBuilder b = new StringBuilder();
196 		for (int i = 0; i < 1024; i++) {
197 			b.append('a');
198 		}
199 		String line = b.toString();
200 		String[] lines = new String[70];
201 		for (int i = 0; i < lines.length; i++) {
202 			lines[i] = line;
203 		}
204 		server.setPreamble(lines);
205 		TransportException e = assertThrows(TransportException.class,
206 				() -> cloneWith(
207 						"ssh://" + TEST_USER + "@localhost:" + testPort
208 								+ "/doesntmatter",
209 						defaultCloneDir, null,
210 						"IdentityFile " + privateKey1.getAbsolutePath()));
211 		// The assertions test that we don't run into bug 565394 / SSHD-1050
212 		assertFalse(e.getMessage().contains("timeout"));
213 		assertTrue(e.getMessage().contains("65536")
214 				|| e.getMessage().contains("closed"));
215 	}
216 
217 	/**
218 	 * Test for SSHD-1028. If the server doesn't close sessions, the second
219 	 * fetch will fail. Occurs on sshd 2.5.[01].
220 	 *
221 	 * @throws Exception
222 	 *             on errors
223 	 * @see <a href=
224 	 *      "https://issues.apache.org/jira/projects/SSHD/issues/SSHD-1028">SSHD-1028</a>
225 	 */
226 	@Test
227 	public void testCloneAndFetchWithSessionLimit() throws Exception {
228 		MAX_CONCURRENT_SESSIONS
229 				.set(server.getPropertyResolver(), Integer.valueOf(2));
230 		File localClone = cloneWith("ssh://localhost/doesntmatter",
231 				defaultCloneDir, null, //
232 				"Host localhost", //
233 				"HostName localhost", //
234 				"Port " + testPort, //
235 				"User " + TEST_USER, //
236 				"IdentityFile " + privateKey1.getAbsolutePath());
237 		// Fetch a couple of times
238 		try (Git git = Git.open(localClone)) {
239 			git.fetch().call();
240 			git.fetch().call();
241 		}
242 	}
243 
244 	/**
245 	 * Creates a simple SSH server without git setup.
246 	 *
247 	 * @param user
248 	 *            to accept
249 	 * @param userKey
250 	 *            public key of that user at this server
251 	 * @return the {@link SshServer}, not yet started
252 	 * @throws Exception
253 	 */
254 	private SshServer createServer(String user, File userKey) throws Exception {
255 		SshServer srv = SshServer.setUpDefaultServer();
256 		// Give the server its own host key
257 		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
258 		generator.initialize(2048);
259 		KeyPair proxyHostKey = generator.generateKeyPair();
260 		srv.setKeyPairProvider(
261 				session -> Collections.singletonList(proxyHostKey));
262 		// Allow (only) publickey authentication
263 		srv.setUserAuthFactories(Collections.singletonList(
264 				ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY));
265 		// Install the user's public key
266 		PublicKey userProxyKey = AuthorizedKeyEntry
267 				.readAuthorizedKeys(userKey.toPath()).get(0)
268 				.resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
269 		srv.setPublickeyAuthenticator(
270 				(userName, publicKey, session) -> user.equals(userName)
271 						&& KeyUtils.compareKeys(userProxyKey, publicKey));
272 		return srv;
273 	}
274 
275 	/**
276 	 * Writes the server's host key to our knownhosts file.
277 	 *
278 	 * @param srv to register
279 	 * @throws Exception
280 	 */
281 	private void registerServer(SshServer srv) throws Exception {
282 		// Add the proxy's host key to knownhosts
283 		try (BufferedWriter writer = Files.newBufferedWriter(
284 				knownHosts.toPath(), StandardCharsets.US_ASCII,
285 				StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
286 			writer.append('\n');
287 			KnownHostHashValue.appendHostPattern(writer, "localhost",
288 					srv.getPort());
289 			writer.append(',');
290 			KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
291 					srv.getPort());
292 			writer.append(' ');
293 			PublicKeyEntry.appendPublicKeyEntry(writer,
294 					srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic());
295 			writer.append('\n');
296 		}
297 	}
298 
299 	/**
300 	 * Creates a simple proxy server. Accepts only publickey authentication from
301 	 * the given user with the given key, allows all forwardings. Adds the
302 	 * proxy's host key to {@link #knownHosts}.
303 	 *
304 	 * @param user
305 	 *            to accept
306 	 * @param userKey
307 	 *            public key of that user at this server
308 	 * @param report
309 	 *            single-element array to report back the forwarded address.
310 	 * @return the started server
311 	 * @throws Exception
312 	 */
313 	private SshServer createProxy(String user, File userKey,
314 			SshdSocketAddress[] report) throws Exception {
315 		SshServer proxy = createServer(user, userKey);
316 		// Allow forwarding
317 		proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) {
318 
319 			@Override
320 			protected boolean checkAcceptance(String request, Session session,
321 					SshdSocketAddress target) {
322 				report[0] = target;
323 				return super.checkAcceptance(request, session, target);
324 			}
325 		});
326 		proxy.start();
327 		registerServer(proxy);
328 		return proxy;
329 	}
330 
331 	@Test
332 	public void testJumpHost() throws Exception {
333 		SshdSocketAddress[] forwarded = { null };
334 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
335 				forwarded)) {
336 			try {
337 				// Now try to clone via the proxy
338 				cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
339 						"Host server", //
340 						"HostName localhost", //
341 						"Port " + testPort, //
342 						"User " + TEST_USER, //
343 						"IdentityFile " + privateKey1.getAbsolutePath(), //
344 						"ProxyJump " + TEST_USER + "X@proxy:" + proxy.getPort(), //
345 						"", //
346 						"Host proxy", //
347 						"Hostname localhost", //
348 						"IdentityFile " + privateKey2.getAbsolutePath());
349 				assertNotNull(forwarded[0]);
350 				assertEquals(testPort, forwarded[0].getPort());
351 			} finally {
352 				proxy.stop();
353 			}
354 		}
355 	}
356 
357 	@Test
358 	public void testJumpHostNone() throws Exception {
359 		// Should not try to go through the non-existing proxy
360 		cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
361 				"Host server", //
362 				"HostName localhost", //
363 				"Port " + testPort, //
364 				"User " + TEST_USER, //
365 				"IdentityFile " + privateKey1.getAbsolutePath(), //
366 				"ProxyJump none", //
367 				"", //
368 				"Host *", //
369 				"ProxyJump " + TEST_USER + "@localhost:1234");
370 	}
371 
372 	@Test
373 	public void testJumpHostWrongKeyAtProxy() throws Exception {
374 		// Test that we find the proxy server's URI in the exception message
375 		SshdSocketAddress[] forwarded = { null };
376 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
377 				forwarded)) {
378 			try {
379 				// Now try to clone via the proxy
380 				TransportException e = assertThrows(TransportException.class,
381 						() -> cloneWith("ssh://server/doesntmatter",
382 								defaultCloneDir, null, //
383 								"Host server", //
384 								"HostName localhost", //
385 								"Port " + testPort, //
386 								"User " + TEST_USER, //
387 								"IdentityFile " + privateKey1.getAbsolutePath(),
388 								"ProxyJump " + TEST_USER + "X@proxy:"
389 										+ proxy.getPort(), //
390 								"", //
391 								"Host proxy", //
392 								"Hostname localhost", //
393 								"IdentityFile "
394 										+ privateKey1.getAbsolutePath()));
395 				String message = e.getMessage();
396 				assertTrue(message.contains("localhost:" + proxy.getPort()));
397 				assertTrue(message.contains("proxy:" + proxy.getPort()));
398 			} finally {
399 				proxy.stop();
400 			}
401 		}
402 	}
403 
404 	@Test
405 	public void testJumpHostWrongKeyAtServer() throws Exception {
406 		// Test that we find the target server's URI in the exception message
407 		SshdSocketAddress[] forwarded = { null };
408 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
409 				forwarded)) {
410 			try {
411 				// Now try to clone via the proxy
412 				TransportException e = assertThrows(TransportException.class,
413 						() -> cloneWith("ssh://server/doesntmatter",
414 								defaultCloneDir, null, //
415 								"Host server", //
416 								"HostName localhost", //
417 								"Port " + testPort, //
418 								"User " + TEST_USER, //
419 								"IdentityFile " + privateKey2.getAbsolutePath(),
420 								"ProxyJump " + TEST_USER + "X@proxy:"
421 										+ proxy.getPort(), //
422 								"", //
423 								"Host proxy", //
424 								"Hostname localhost", //
425 								"IdentityFile "
426 										+ privateKey2.getAbsolutePath()));
427 				String message = e.getMessage();
428 				assertTrue(message.contains("localhost:" + testPort));
429 				assertTrue(message.contains("ssh://server"));
430 			} finally {
431 				proxy.stop();
432 			}
433 		}
434 	}
435 
436 	@Test
437 	public void testJumpHostNonSsh() throws Exception {
438 		SshdSocketAddress[] forwarded = { null };
439 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
440 				forwarded)) {
441 			try {
442 				TransportException e = assertThrows(TransportException.class,
443 						() -> cloneWith("ssh://server/doesntmatter",
444 								defaultCloneDir, null, //
445 								"Host server", //
446 								"HostName localhost", //
447 								"Port " + testPort, //
448 								"User " + TEST_USER, //
449 								"IdentityFile " + privateKey1.getAbsolutePath(), //
450 								"ProxyJump http://" + TEST_USER + "X@proxy:"
451 										+ proxy.getPort(), //
452 								"", //
453 								"Host proxy", //
454 								"Hostname localhost", //
455 								"IdentityFile "
456 										+ privateKey2.getAbsolutePath()));
457 				// Find the expected message
458 				Throwable t = e;
459 				while (t != null) {
460 					if (t instanceof URISyntaxException) {
461 						break;
462 					}
463 					t = t.getCause();
464 				}
465 				assertNotNull(t);
466 				assertTrue(t.getMessage().contains("Non-ssh"));
467 			} finally {
468 				proxy.stop();
469 			}
470 		}
471 	}
472 
473 	@Test
474 	public void testJumpHostWithPath() throws Exception {
475 		SshdSocketAddress[] forwarded = { null };
476 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
477 				forwarded)) {
478 			try {
479 				TransportException e = assertThrows(TransportException.class,
480 						() -> cloneWith("ssh://server/doesntmatter",
481 								defaultCloneDir, null, //
482 								"Host server", //
483 								"HostName localhost", //
484 								"Port " + testPort, //
485 								"User " + TEST_USER, //
486 								"IdentityFile " + privateKey1.getAbsolutePath(), //
487 								"ProxyJump ssh://" + TEST_USER + "X@proxy:"
488 										+ proxy.getPort() + "/wrongPath", //
489 								"", //
490 								"Host proxy", //
491 								"Hostname localhost", //
492 								"IdentityFile "
493 										+ privateKey2.getAbsolutePath()));
494 				// Find the expected message
495 				Throwable t = e;
496 				while (t != null) {
497 					if (t instanceof URISyntaxException) {
498 						break;
499 					}
500 					t = t.getCause();
501 				}
502 				assertNotNull(t);
503 				assertTrue(t.getMessage().contains("wrongPath"));
504 			} finally {
505 				proxy.stop();
506 			}
507 		}
508 	}
509 
510 	@Test
511 	public void testJumpHostWithPathShort() throws Exception {
512 		SshdSocketAddress[] forwarded = { null };
513 		try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2,
514 				forwarded)) {
515 			try {
516 				TransportException e = assertThrows(TransportException.class,
517 						() -> cloneWith("ssh://server/doesntmatter",
518 								defaultCloneDir, null, //
519 								"Host server", //
520 								"HostName localhost", //
521 								"Port " + testPort, //
522 								"User " + TEST_USER, //
523 								"IdentityFile " + privateKey1.getAbsolutePath(), //
524 								"ProxyJump " + TEST_USER + "X@proxy:wrongPath", //
525 								"", //
526 								"Host proxy", //
527 								"Hostname localhost", //
528 								"Port " + proxy.getPort(), //
529 								"IdentityFile "
530 										+ privateKey2.getAbsolutePath()));
531 				// Find the expected message
532 				Throwable t = e;
533 				while (t != null) {
534 					if (t instanceof URISyntaxException) {
535 						break;
536 					}
537 					t = t.getCause();
538 				}
539 				assertNotNull(t);
540 				assertTrue(t.getMessage().contains("wrongPath"));
541 			} finally {
542 				proxy.stop();
543 			}
544 		}
545 	}
546 
547 	@Test
548 	public void testJumpHostChain() throws Exception {
549 		SshdSocketAddress[] forwarded1 = { null };
550 		SshdSocketAddress[] forwarded2 = { null };
551 		try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
552 				forwarded1);
553 				SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
554 			try {
555 				// Clone proxy1 -> proxy2 -> server
556 				cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
557 						"Host server", //
558 						"HostName localhost", //
559 						"Port " + testPort, //
560 						"User " + TEST_USER, //
561 						"IdentityFile " + privateKey1.getAbsolutePath(), //
562 						"ProxyJump proxy2," + TEST_USER + "X@proxy:"
563 								+ proxy1.getPort(), //
564 						"", //
565 						"Host proxy", //
566 						"Hostname localhost", //
567 						"IdentityFile " + privateKey2.getAbsolutePath(), //
568 						"", //
569 						"Host proxy2", //
570 						"Hostname localhost", //
571 						"User foo", //
572 						"Port " + proxy2.getPort(), //
573 						"IdentityFile " + privateKey1.getAbsolutePath());
574 				assertNotNull(forwarded1[0]);
575 				assertEquals(proxy2.getPort(), forwarded1[0].getPort());
576 				assertNotNull(forwarded2[0]);
577 				assertEquals(testPort, forwarded2[0].getPort());
578 			} finally {
579 				proxy1.stop();
580 				proxy2.stop();
581 			}
582 		}
583 	}
584 
585 	@Test
586 	public void testJumpHostCascade() throws Exception {
587 		SshdSocketAddress[] forwarded1 = { null };
588 		SshdSocketAddress[] forwarded2 = { null };
589 		try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
590 				forwarded1);
591 				SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
592 			try {
593 				// Clone proxy2 -> proxy1 -> server
594 				cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, //
595 						"Host server", //
596 						"HostName localhost", //
597 						"Port " + testPort, //
598 						"User " + TEST_USER, //
599 						"IdentityFile " + privateKey1.getAbsolutePath(), //
600 						"ProxyJump " + TEST_USER + "X@proxy", //
601 						"", //
602 						"Host proxy", //
603 						"Hostname localhost", //
604 						"Port " + proxy1.getPort(), //
605 						"ProxyJump ssh://proxy2:" + proxy2.getPort(), //
606 						"IdentityFile " + privateKey2.getAbsolutePath(), //
607 						"", //
608 						"Host proxy2", //
609 						"Hostname localhost", //
610 						"User foo", //
611 						"IdentityFile " + privateKey1.getAbsolutePath());
612 				assertNotNull(forwarded1[0]);
613 				assertEquals(testPort, forwarded1[0].getPort());
614 				assertNotNull(forwarded2[0]);
615 				assertEquals(proxy1.getPort(), forwarded2[0].getPort());
616 			} finally {
617 				proxy1.stop();
618 				proxy2.stop();
619 			}
620 		}
621 	}
622 
623 	@Test
624 	public void testJumpHostRecursion() throws Exception {
625 		SshdSocketAddress[] forwarded1 = { null };
626 		SshdSocketAddress[] forwarded2 = { null };
627 		try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2,
628 				forwarded1);
629 				SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) {
630 			try {
631 				TransportException e = assertThrows(TransportException.class,
632 						() -> cloneWith(
633 						"ssh://server/doesntmatter", defaultCloneDir, null, //
634 						"Host server", //
635 						"HostName localhost", //
636 						"Port " + testPort, //
637 						"User " + TEST_USER, //
638 						"IdentityFile " + privateKey1.getAbsolutePath(), //
639 						"ProxyJump " + TEST_USER + "X@proxy", //
640 						"", //
641 						"Host proxy", //
642 						"Hostname localhost", //
643 						"Port " + proxy1.getPort(), //
644 						"ProxyJump ssh://proxy2:" + proxy2.getPort(), //
645 						"IdentityFile " + privateKey2.getAbsolutePath(), //
646 						"", //
647 						"Host proxy2", //
648 						"Hostname localhost", //
649 						"User foo", //
650 						"ProxyJump " + TEST_USER + "X@proxy", //
651 						"IdentityFile " + privateKey1.getAbsolutePath()));
652 				assertTrue(e.getMessage().contains("proxy"));
653 			} finally {
654 				proxy1.stop();
655 				proxy2.stop();
656 			}
657 		}
658 	}
659 
660 	/**
661 	 * Tests that one can log in to an old server that doesn't handle
662 	 * rsa-sha2-512 if one puts ssh-rsa first in the client's list of public key
663 	 * signature algorithms.
664 	 *
665 	 * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug
666 	 *      572056</a>
667 	 * @throws Exception
668 	 *             on failure
669 	 */
670 	@Test
671 	public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms()
672 			throws Exception {
673 		try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
674 			oldServer.setSignatureFactoriesNames("ssh-rsa");
675 			oldServer.start();
676 			registerServer(oldServer);
677 			installConfig("Host server", //
678 					"HostName localhost", //
679 					"Port " + oldServer.getPort(), //
680 					"User " + TEST_USER, //
681 					"IdentityFile " + privateKey1.getAbsolutePath(), //
682 					"PubkeyAcceptedAlgorithms ^ssh-rsa");
683 			RemoteSession session = getSessionFactory().getSession(
684 					new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
685 					10000);
686 			assertNotNull(session);
687 			session.disconnect();
688 		}
689 	}
690 
691 	/**
692 	 * Tests that one can log in to an old server that knows only the ssh-rsa
693 	 * signature algorithm. The client has by default the list of signature
694 	 * algorithms for RSA as "rsa-sha2-512,rsa-sha2-256,ssh-rsa". It should try
695 	 * all three with the single key configured, and finally succeed.
696 	 * <p>
697 	 * The re-ordering mechanism (see
698 	 * {@link #testConnectAuthSshRsaPubkeyAcceptedAlgorithms()}) is still
699 	 * important; servers may impose a penalty (back-off delay) for subsequent
700 	 * attempts with signature algorithms unknown to the server. So a user
701 	 * connecting to such a server and noticing delays may still want to put
702 	 * ssh-rsa first in the list for that host.
703 	 * </p>
704 	 *
705 	 * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug
706 	 *      572056</a>
707 	 * @throws Exception
708 	 *             on failure
709 	 */
710 	@Test
711 	public void testConnectAuthSshRsa() throws Exception {
712 		try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
713 			oldServer.setSignatureFactoriesNames("ssh-rsa");
714 			oldServer.start();
715 			registerServer(oldServer);
716 			installConfig("Host server", //
717 					"HostName localhost", //
718 					"Port " + oldServer.getPort(), //
719 					"User " + TEST_USER, //
720 					"IdentityFile " + privateKey1.getAbsolutePath());
721 			RemoteSession session = getSessionFactory().getSession(
722 					new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
723 					10000);
724 			assertNotNull(session);
725 			session.disconnect();
726 		}
727 	}
728 
729 	/**
730 	 * Tests that one can log in at an even poorer server that also only has the
731 	 * SHA1 KEX methods available. Apparently this is the case for at least some
732 	 * Microsoft TFS instances. The user has to enable the poor KEX methods in
733 	 * the ssh config explicitly; we don't enable them by default.
734 	 *
735 	 * @throws Exception
736 	 *             on failure
737 	 */
738 	@Test
739 	public void testConnectOnlyRsaSha1() throws Exception {
740 		try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
741 			oldServer.setSignatureFactoriesNames("ssh-rsa");
742 			List<DHFactory> sha1Factories = BuiltinDHFactories
743 					.parseDHFactoriesList(
744 							"diffie-hellman-group1-sha1,diffie-hellman-group14-sha1")
745 					.getParsedFactories();
746 			assertEquals(2, sha1Factories.size());
747 			List<KeyExchangeFactory> kexFactories = NamedFactory
748 					.setUpTransformedFactories(true, sha1Factories,
749 							ServerBuilder.DH2KEX);
750 			oldServer.setKeyExchangeFactories(kexFactories);
751 			oldServer.start();
752 			registerServer(oldServer);
753 			installConfig("Host server", //
754 					"HostName localhost", //
755 					"Port " + oldServer.getPort(), //
756 					"User " + TEST_USER, //
757 					"IdentityFile " + privateKey1.getAbsolutePath(), //
758 					"KexAlgorithms +diffie-hellman-group1-sha1");
759 			RemoteSession session = getSessionFactory().getSession(
760 					new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
761 					10000);
762 			assertNotNull(session);
763 			session.disconnect();
764 		}
765 	}
766 }