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