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