| /* |
| * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| package org.eclipse.jgit.transport.sshd; |
| |
| import static org.apache.sshd.core.CoreModuleProperties.MAX_CONCURRENT_SESSIONS; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertThrows; |
| import static org.junit.Assert.assertTrue; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.net.URISyntaxException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.StandardOpenOption; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.PublicKey; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| import org.apache.sshd.client.config.hosts.KnownHostEntry; |
| import org.apache.sshd.client.config.hosts.KnownHostHashValue; |
| import org.apache.sshd.common.NamedFactory; |
| import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; |
| import org.apache.sshd.common.config.keys.KeyUtils; |
| import org.apache.sshd.common.config.keys.PublicKeyEntry; |
| import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; |
| import org.apache.sshd.common.kex.BuiltinDHFactories; |
| import org.apache.sshd.common.kex.DHFactory; |
| import org.apache.sshd.common.kex.KeyExchangeFactory; |
| import org.apache.sshd.common.session.Session; |
| import org.apache.sshd.common.util.net.SshdSocketAddress; |
| import org.apache.sshd.server.ServerAuthenticationManager; |
| import org.apache.sshd.server.ServerBuilder; |
| import org.apache.sshd.server.SshServer; |
| import org.apache.sshd.server.forward.StaticDecisionForwardingFilter; |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.api.errors.TransportException; |
| import org.eclipse.jgit.junit.ssh.SshTestBase; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.transport.RemoteSession; |
| import org.eclipse.jgit.transport.SshSessionFactory; |
| import org.eclipse.jgit.transport.URIish; |
| import org.eclipse.jgit.util.FS; |
| import org.junit.Test; |
| import org.junit.experimental.theories.Theories; |
| import org.junit.runner.RunWith; |
| |
| @RunWith(Theories.class) |
| public class ApacheSshTest extends SshTestBase { |
| |
| @Override |
| protected SshSessionFactory createSessionFactory() { |
| return new SshdSessionFactoryBuilder() |
| // No proxies in tests |
| .setProxyDataFactory(null) |
| // No ssh-agent in tests |
| .setConnectorFactory(null) |
| // The home directory is mocked at this point! |
| .setHomeDirectory(FS.DETECTED.userHome()) |
| .setSshDirectory(sshDir) |
| .build(new JGitKeyCache()); |
| } |
| |
| @Override |
| protected void installConfig(String... config) { |
| File configFile = new File(sshDir, Constants.CONFIG); |
| if (config != null) { |
| try { |
| Files.write(configFile.toPath(), Arrays.asList(config)); |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| } |
| } |
| |
| @Test |
| public void testEd25519HostKey() throws Exception { |
| // Using ed25519 user identities is tested in the super class in |
| // testSshKeys(). |
| File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); |
| copyTestResource("id_ed25519", newHostKey); |
| server.addHostKey(newHostKey.toPath(), true); |
| File newHostKeyPub = new File(getTemporaryDirectory(), |
| "newhostkey.pub"); |
| copyTestResource("id_ed25519.pub", newHostKeyPub); |
| createKnownHostsFile(knownHosts, "localhost", testPort, newHostKeyPub); |
| cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, // |
| "Host git", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| } |
| |
| /** |
| * Test for SSHD-1231. If authentication is attempted first with an RSA key, |
| * which is rejected, and then with some other key type (here ed25519), |
| * authentication fails in bug SSHD-1231. |
| * |
| * @throws Exception |
| * on errors |
| * @see <a href= |
| * "https://issues.apache.org/jira/browse/SSHD-1231">SSHD-1231</a> |
| */ |
| @Test |
| public void testWrongKeyFirst() throws Exception { |
| File userKey = new File(getTemporaryDirectory(), "userkey"); |
| copyTestResource("id_ed25519", userKey); |
| File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); |
| copyTestResource("id_ed25519.pub", publicKey); |
| server.setTestUserPublicKey(publicKey.toPath()); |
| cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, // |
| "Host git", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // RSA |
| "IdentityFile " + userKey.getAbsolutePath()); |
| } |
| |
| @Test |
| public void testHashedKnownHosts() throws Exception { |
| assertTrue("Failed to delete known_hosts", knownHosts.delete()); |
| // The provider will answer "yes" to all questions, so we should be able |
| // to connect and end up with a new known_hosts file with the host key. |
| TestCredentialsProvider provider = new TestCredentialsProvider(); |
| cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, // |
| "HashKnownHosts yes", // |
| "Host localhost", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| List<LogEntry> messages = provider.getLog(); |
| assertFalse("Expected user interaction", messages.isEmpty()); |
| assertEquals( |
| "Expected to be asked about the key, and the file creation", 2, |
| messages.size()); |
| assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists()); |
| // Let's clone again without provider. If it works, the server host key |
| // was written correctly. |
| File clonedAgain = new File(getTemporaryDirectory(), "cloned2"); |
| cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, // |
| "Host localhost", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| // Check that the first line contains neither "localhost" nor |
| // "127.0.0.1", but does contain the expected hash. |
| List<String> lines = Files.readAllLines(knownHosts.toPath()).stream() |
| .filter(s -> s != null && s.length() >= 1 && s.charAt(0) != '#' |
| && !s.trim().isEmpty()) |
| .collect(Collectors.toList()); |
| assertEquals("Unexpected number of known_hosts lines", 1, lines.size()); |
| String line = lines.get(0); |
| assertFalse("Found host in line", line.contains("localhost")); |
| assertFalse("Found IP in line", line.contains("127.0.0.1")); |
| assertTrue("Hash not found", line.contains("|")); |
| KnownHostEntry entry = KnownHostEntry.parseKnownHostEntry(line); |
| assertTrue("Hash doesn't match localhost", |
| entry.isHostMatch("localhost", testPort) |
| || entry.isHostMatch("127.0.0.1", testPort)); |
| } |
| |
| @Test |
| public void testPreamble() throws Exception { |
| // Test that the client can deal with strange lines being sent before |
| // the server identification string. |
| StringBuilder b = new StringBuilder(); |
| for (int i = 0; i < 257; i++) { |
| b.append('a'); |
| } |
| server.setPreamble("A line with a \000 NUL", |
| "A long line: " + b.toString()); |
| cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| } |
| |
| @Test |
| public void testLongPreamble() throws Exception { |
| // Test that the client can deal with a long (about 60k) preamble. |
| StringBuilder b = new StringBuilder(); |
| for (int i = 0; i < 1024; i++) { |
| b.append('a'); |
| } |
| String line = b.toString(); |
| String[] lines = new String[60]; |
| for (int i = 0; i < lines.length; i++) { |
| lines[i] = line; |
| } |
| server.setPreamble(lines); |
| cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| } |
| |
| @Test |
| public void testHugePreamble() throws Exception { |
| // Test that the connection fails when the preamble is longer than 64k. |
| StringBuilder b = new StringBuilder(); |
| for (int i = 0; i < 1024; i++) { |
| b.append('a'); |
| } |
| String line = b.toString(); |
| String[] lines = new String[70]; |
| for (int i = 0; i < lines.length; i++) { |
| lines[i] = line; |
| } |
| server.setPreamble(lines); |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath())); |
| // The assertions test that we don't run into bug 565394 / SSHD-1050 |
| assertFalse(e.getMessage().contains("timeout")); |
| assertTrue(e.getMessage().contains("65536") |
| || e.getMessage().contains("closed")); |
| } |
| |
| /** |
| * Test for SSHD-1028. If the server doesn't close sessions, the second |
| * fetch will fail. Occurs on sshd 2.5.[01]. |
| * |
| * @throws Exception |
| * on errors |
| * @see <a href= |
| * "https://issues.apache.org/jira/projects/SSHD/issues/SSHD-1028">SSHD-1028</a> |
| */ |
| @Test |
| public void testCloneAndFetchWithSessionLimit() throws Exception { |
| MAX_CONCURRENT_SESSIONS |
| .set(server.getPropertyResolver(), Integer.valueOf(2)); |
| File localClone = cloneWith("ssh://localhost/doesntmatter", |
| defaultCloneDir, null, // |
| "Host localhost", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| // Fetch a couple of times |
| try (Git git = Git.open(localClone)) { |
| git.fetch().call(); |
| git.fetch().call(); |
| } |
| } |
| |
| /** |
| * Creates a simple SSH server without git setup. |
| * |
| * @param user |
| * to accept |
| * @param userKey |
| * public key of that user at this server |
| * @return the {@link SshServer}, not yet started |
| * @throws Exception |
| */ |
| private SshServer createServer(String user, File userKey) throws Exception { |
| SshServer srv = SshServer.setUpDefaultServer(); |
| // Give the server its own host key |
| KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); |
| generator.initialize(2048); |
| KeyPair proxyHostKey = generator.generateKeyPair(); |
| srv.setKeyPairProvider( |
| session -> Collections.singletonList(proxyHostKey)); |
| // Allow (only) publickey authentication |
| srv.setUserAuthFactories(Collections.singletonList( |
| ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY)); |
| // Install the user's public key |
| PublicKey userProxyKey = AuthorizedKeyEntry |
| .readAuthorizedKeys(userKey.toPath()).get(0) |
| .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING); |
| srv.setPublickeyAuthenticator( |
| (userName, publicKey, session) -> user.equals(userName) |
| && KeyUtils.compareKeys(userProxyKey, publicKey)); |
| return srv; |
| } |
| |
| /** |
| * Writes the server's host key to our knownhosts file. |
| * |
| * @param srv to register |
| * @throws Exception |
| */ |
| private void registerServer(SshServer srv) throws Exception { |
| // Add the proxy's host key to knownhosts |
| try (BufferedWriter writer = Files.newBufferedWriter( |
| knownHosts.toPath(), StandardCharsets.US_ASCII, |
| StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { |
| writer.append('\n'); |
| KnownHostHashValue.appendHostPattern(writer, "localhost", |
| srv.getPort()); |
| writer.append(','); |
| KnownHostHashValue.appendHostPattern(writer, "127.0.0.1", |
| srv.getPort()); |
| writer.append(' '); |
| PublicKeyEntry.appendPublicKeyEntry(writer, |
| srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic()); |
| writer.append('\n'); |
| } |
| } |
| |
| /** |
| * Creates a simple proxy server. Accepts only publickey authentication from |
| * the given user with the given key, allows all forwardings. Adds the |
| * proxy's host key to {@link #knownHosts}. |
| * |
| * @param user |
| * to accept |
| * @param userKey |
| * public key of that user at this server |
| * @param report |
| * single-element array to report back the forwarded address. |
| * @return the started server |
| * @throws Exception |
| */ |
| private SshServer createProxy(String user, File userKey, |
| SshdSocketAddress[] report) throws Exception { |
| SshServer proxy = createServer(user, userKey); |
| // Allow forwarding |
| proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) { |
| |
| @Override |
| protected boolean checkAcceptance(String request, Session session, |
| SshdSocketAddress target) { |
| report[0] = target; |
| return super.checkAcceptance(request, session, target); |
| } |
| }); |
| proxy.start(); |
| registerServer(proxy); |
| return proxy; |
| } |
| |
| @Test |
| public void testJumpHost() throws Exception { |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| // Now try to clone via the proxy |
| cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump " + TEST_USER + "X@proxy:" + proxy.getPort(), // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " + privateKey2.getAbsolutePath()); |
| assertNotNull(forwarded[0]); |
| assertEquals(testPort, forwarded[0].getPort()); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostNone() throws Exception { |
| // Should not try to go through the non-existing proxy |
| cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump none", // |
| "", // |
| "Host *", // |
| "ProxyJump " + TEST_USER + "@localhost:1234"); |
| } |
| |
| @Test |
| public void testJumpHostWrongKeyAtProxy() throws Exception { |
| // Test that we find the proxy server's URI in the exception message |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| // Now try to clone via the proxy |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://server/doesntmatter", |
| defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), |
| "ProxyJump " + TEST_USER + "X@proxy:" |
| + proxy.getPort(), // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " |
| + privateKey1.getAbsolutePath())); |
| String message = e.getMessage(); |
| assertTrue(message.contains("localhost:" + proxy.getPort())); |
| assertTrue(message.contains("proxy:" + proxy.getPort())); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostWrongKeyAtServer() throws Exception { |
| // Test that we find the target server's URI in the exception message |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| // Now try to clone via the proxy |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://server/doesntmatter", |
| defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey2.getAbsolutePath(), |
| "ProxyJump " + TEST_USER + "X@proxy:" |
| + proxy.getPort(), // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " |
| + privateKey2.getAbsolutePath())); |
| String message = e.getMessage(); |
| assertTrue(message.contains("localhost:" + testPort)); |
| assertTrue(message.contains("ssh://server")); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostNonSsh() throws Exception { |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://server/doesntmatter", |
| defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump http://" + TEST_USER + "X@proxy:" |
| + proxy.getPort(), // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " |
| + privateKey2.getAbsolutePath())); |
| // Find the expected message |
| Throwable t = e; |
| while (t != null) { |
| if (t instanceof URISyntaxException) { |
| break; |
| } |
| t = t.getCause(); |
| } |
| assertNotNull(t); |
| assertTrue(t.getMessage().contains("Non-ssh")); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostWithPath() throws Exception { |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://server/doesntmatter", |
| defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump ssh://" + TEST_USER + "X@proxy:" |
| + proxy.getPort() + "/wrongPath", // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " |
| + privateKey2.getAbsolutePath())); |
| // Find the expected message |
| Throwable t = e; |
| while (t != null) { |
| if (t instanceof URISyntaxException) { |
| break; |
| } |
| t = t.getCause(); |
| } |
| assertNotNull(t); |
| assertTrue(t.getMessage().contains("wrongPath")); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostWithPathShort() throws Exception { |
| SshdSocketAddress[] forwarded = { null }; |
| try (SshServer proxy = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded)) { |
| try { |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://server/doesntmatter", |
| defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump " + TEST_USER + "X@proxy:wrongPath", // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "Port " + proxy.getPort(), // |
| "IdentityFile " |
| + privateKey2.getAbsolutePath())); |
| // Find the expected message |
| Throwable t = e; |
| while (t != null) { |
| if (t instanceof URISyntaxException) { |
| break; |
| } |
| t = t.getCause(); |
| } |
| assertNotNull(t); |
| assertTrue(t.getMessage().contains("wrongPath")); |
| } finally { |
| proxy.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostChain() throws Exception { |
| SshdSocketAddress[] forwarded1 = { null }; |
| SshdSocketAddress[] forwarded2 = { null }; |
| try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded1); |
| SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) { |
| try { |
| // Clone proxy1 -> proxy2 -> server |
| cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump proxy2," + TEST_USER + "X@proxy:" |
| + proxy1.getPort(), // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "IdentityFile " + privateKey2.getAbsolutePath(), // |
| "", // |
| "Host proxy2", // |
| "Hostname localhost", // |
| "User foo", // |
| "Port " + proxy2.getPort(), // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| assertNotNull(forwarded1[0]); |
| assertEquals(proxy2.getPort(), forwarded1[0].getPort()); |
| assertNotNull(forwarded2[0]); |
| assertEquals(testPort, forwarded2[0].getPort()); |
| } finally { |
| proxy1.stop(); |
| proxy2.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostCascade() throws Exception { |
| SshdSocketAddress[] forwarded1 = { null }; |
| SshdSocketAddress[] forwarded2 = { null }; |
| try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded1); |
| SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) { |
| try { |
| // Clone proxy2 -> proxy1 -> server |
| cloneWith("ssh://server/doesntmatter", defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump " + TEST_USER + "X@proxy", // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "Port " + proxy1.getPort(), // |
| "ProxyJump ssh://proxy2:" + proxy2.getPort(), // |
| "IdentityFile " + privateKey2.getAbsolutePath(), // |
| "", // |
| "Host proxy2", // |
| "Hostname localhost", // |
| "User foo", // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| assertNotNull(forwarded1[0]); |
| assertEquals(testPort, forwarded1[0].getPort()); |
| assertNotNull(forwarded2[0]); |
| assertEquals(proxy1.getPort(), forwarded2[0].getPort()); |
| } finally { |
| proxy1.stop(); |
| proxy2.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testJumpHostRecursion() throws Exception { |
| SshdSocketAddress[] forwarded1 = { null }; |
| SshdSocketAddress[] forwarded2 = { null }; |
| try (SshServer proxy1 = createProxy(TEST_USER + 'X', publicKey2, |
| forwarded1); |
| SshServer proxy2 = createProxy("foo", publicKey1, forwarded2)) { |
| try { |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith( |
| "ssh://server/doesntmatter", defaultCloneDir, null, // |
| "Host server", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "ProxyJump " + TEST_USER + "X@proxy", // |
| "", // |
| "Host proxy", // |
| "Hostname localhost", // |
| "Port " + proxy1.getPort(), // |
| "ProxyJump ssh://proxy2:" + proxy2.getPort(), // |
| "IdentityFile " + privateKey2.getAbsolutePath(), // |
| "", // |
| "Host proxy2", // |
| "Hostname localhost", // |
| "User foo", // |
| "ProxyJump " + TEST_USER + "X@proxy", // |
| "IdentityFile " + privateKey1.getAbsolutePath())); |
| assertTrue(e.getMessage().contains("proxy")); |
| } finally { |
| proxy1.stop(); |
| proxy2.stop(); |
| } |
| } |
| } |
| |
| /** |
| * Tests that one can log in to an old server that doesn't handle |
| * rsa-sha2-512 if one puts ssh-rsa first in the client's list of public key |
| * signature algorithms. |
| * |
| * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug |
| * 572056</a> |
| * @throws Exception |
| * on failure |
| */ |
| @Test |
| public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms() |
| throws Exception { |
| try (SshServer oldServer = createServer(TEST_USER, publicKey1)) { |
| oldServer.setSignatureFactoriesNames("ssh-rsa"); |
| oldServer.start(); |
| registerServer(oldServer); |
| installConfig("Host server", // |
| "HostName localhost", // |
| "Port " + oldServer.getPort(), // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "PubkeyAcceptedAlgorithms ^ssh-rsa"); |
| RemoteSession session = getSessionFactory().getSession( |
| new URIish("ssh://server/doesntmatter"), null, FS.DETECTED, |
| 10000); |
| assertNotNull(session); |
| session.disconnect(); |
| } |
| } |
| |
| /** |
| * Tests that one can log in to an old server that knows only the ssh-rsa |
| * signature algorithm. The client has by default the list of signature |
| * algorithms for RSA as "rsa-sha2-512,rsa-sha2-256,ssh-rsa". It should try |
| * all three with the single key configured, and finally succeed. |
| * <p> |
| * The re-ordering mechanism (see |
| * {@link #testConnectAuthSshRsaPubkeyAcceptedAlgorithms()}) is still |
| * important; servers may impose a penalty (back-off delay) for subsequent |
| * attempts with signature algorithms unknown to the server. So a user |
| * connecting to such a server and noticing delays may still want to put |
| * ssh-rsa first in the list for that host. |
| * </p> |
| * |
| * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug |
| * 572056</a> |
| * @throws Exception |
| * on failure |
| */ |
| @Test |
| public void testConnectAuthSshRsa() throws Exception { |
| try (SshServer oldServer = createServer(TEST_USER, publicKey1)) { |
| oldServer.setSignatureFactoriesNames("ssh-rsa"); |
| oldServer.start(); |
| registerServer(oldServer); |
| installConfig("Host server", // |
| "HostName localhost", // |
| "Port " + oldServer.getPort(), // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath()); |
| RemoteSession session = getSessionFactory().getSession( |
| new URIish("ssh://server/doesntmatter"), null, FS.DETECTED, |
| 10000); |
| assertNotNull(session); |
| session.disconnect(); |
| } |
| } |
| |
| /** |
| * Tests that one can log in at an even poorer server that also only has the |
| * SHA1 KEX methods available. Apparently this is the case for at least some |
| * Microsoft TFS instances. The user has to enable the poor KEX methods in |
| * the ssh config explicitly; we don't enable them by default. |
| * |
| * @throws Exception |
| * on failure |
| */ |
| @Test |
| public void testConnectOnlyRsaSha1() throws Exception { |
| try (SshServer oldServer = createServer(TEST_USER, publicKey1)) { |
| oldServer.setSignatureFactoriesNames("ssh-rsa"); |
| List<DHFactory> sha1Factories = BuiltinDHFactories |
| .parseDHFactoriesList( |
| "diffie-hellman-group1-sha1,diffie-hellman-group14-sha1") |
| .getParsedFactories(); |
| assertEquals(2, sha1Factories.size()); |
| List<KeyExchangeFactory> kexFactories = NamedFactory |
| .setUpTransformedFactories(true, sha1Factories, |
| ServerBuilder.DH2KEX); |
| oldServer.setKeyExchangeFactories(kexFactories); |
| oldServer.start(); |
| registerServer(oldServer); |
| installConfig("Host server", // |
| "HostName localhost", // |
| "Port " + oldServer.getPort(), // |
| "User " + TEST_USER, // |
| "IdentityFile " + privateKey1.getAbsolutePath(), // |
| "KexAlgorithms +diffie-hellman-group1-sha1"); |
| RemoteSession session = getSessionFactory().getSession( |
| new URIish("ssh://server/doesntmatter"), null, FS.DETECTED, |
| 10000); |
| assertNotNull(session); |
| session.disconnect(); |
| } |
| } |
| |
| private void verifyAuthLog(String message, String first) { |
| assertTrue(message.contains(System.lineSeparator())); |
| String[] lines = message.split(System.lineSeparator()); |
| int pubkeyIndex = -1; |
| int passwordIndex = -1; |
| for (int i = 0; i < lines.length; i++) { |
| String line = lines[i]; |
| if (i == 0) { |
| assertTrue(line.contains(first)); |
| } |
| if (line.contains("publickey:")) { |
| if (pubkeyIndex < 0) { |
| pubkeyIndex = i; |
| assertTrue(line.contains("/userkey")); |
| } |
| } else if (line.contains("password:")) { |
| if (passwordIndex < 0) { |
| passwordIndex = i; |
| assertTrue(line.contains("attempt 1")); |
| } |
| } |
| } |
| assertTrue(pubkeyIndex > 0 && passwordIndex > 0); |
| assertTrue(pubkeyIndex < passwordIndex); |
| } |
| |
| @Test |
| public void testAuthFailureMessageCancel() throws Exception { |
| File userKey = new File(getTemporaryDirectory(), "userkey"); |
| copyTestResource("id_ed25519", userKey); |
| File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); |
| copyTestResource("id_ed25519.pub", publicKey); |
| // Don't set this as the user's key; we do want to try with a wrong key. |
| server.enablePasswordAuthentication(); |
| TestCredentialsProvider provider = new TestCredentialsProvider( |
| "wrongpass"); |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://git/doesntmatter", defaultCloneDir, |
| provider, // |
| "Host git", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + userKey.getAbsolutePath(), // |
| "PreferredAuthentications publickey,password")); |
| verifyAuthLog(e.getMessage(), "canceled"); |
| } |
| |
| @Test |
| public void testAuthFailureMessage() throws Exception { |
| File userKey = new File(getTemporaryDirectory(), "userkey"); |
| copyTestResource("id_ed25519", userKey); |
| File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); |
| copyTestResource("id_ed25519.pub", publicKey); |
| // Don't set this as the user's key; we do want to try with a wrong key. |
| server.enablePasswordAuthentication(); |
| // Enough passwords not to cancel authentication |
| TestCredentialsProvider provider = new TestCredentialsProvider( |
| "wrongpass", "wrongpass", "wrongpass"); |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith("ssh://git/doesntmatter", defaultCloneDir, |
| provider, // |
| "Host git", // |
| "HostName localhost", // |
| "Port " + testPort, // |
| "User " + TEST_USER, // |
| "IdentityFile " + userKey.getAbsolutePath(), // |
| "PreferredAuthentications publickey,password")); |
| verifyAuthLog(e.getMessage(), "log in"); |
| } |
| |
| @Test |
| public void testCipherModificationSingle() throws Exception { |
| cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath(), |
| "Ciphers aes192-ctr"); |
| } |
| |
| @Test |
| public void testCipherModificationAdd() throws Exception { |
| cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath(), |
| "Ciphers +3des-cbc"); |
| } |
| |
| @Test |
| public void testCipherModificationUnknown() throws Exception { |
| TransportException e = assertThrows(TransportException.class, |
| () -> cloneWith( |
| "ssh://" + TEST_USER + "@localhost:" + testPort |
| + "/doesntmatter", |
| defaultCloneDir, null, |
| "IdentityFile " + privateKey1.getAbsolutePath(), |
| // The server is not configured to use this deprecated |
| // algorithm |
| "Ciphers 3des-cbc")); |
| assertTrue(e.getLocalizedMessage().contains("3des-cbc")); |
| } |
| } |