summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache.test
diff options
context:
space:
mode:
authorThomas Wolf <thomas.wolf@paranor.ch>2020-07-26 20:37:57 +0200
committerThomas Wolf <thomas.wolf@paranor.ch>2020-09-19 15:17:00 -0400
commit566e49d7d39b12c785be24b8b61b4960a4b7ea17 (patch)
treee85e18f6feed63d84a8a8be09cd6179bad97930f /org.eclipse.jgit.ssh.apache.test
parent020dc586a6e01fd98f0ce8ca0c0c9997b4224fc4 (diff)
downloadjgit-566e49d7d39b12c785be24b8b61b4960a4b7ea17.tar.gz
jgit-566e49d7d39b12c785be24b8b61b4960a4b7ea17.zip
sshd: support the ProxyJump ssh config
This is useful to access git repositories behind a bastion server (jump host). Add a constant for the config; rewrite the whole connection initiation to parse the value and (recursively) set up the chain of hops. Add tests for a single hop and two different ways to configure a two-hop chain. The connection timeout applies to each hop in the chain individually. Change-Id: Idd25af95aa2ec5367404587e4e530b0663c03665 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.jgit.ssh.apache.test')
-rw-r--r--org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java398
2 files changed, 399 insertions, 1 deletions
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
index 47f00695de..60f7d41a62 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -11,11 +11,13 @@ Import-Package: org.apache.sshd.client.config.hosts;version="[2.4.0,2.5.0)",
org.apache.sshd.common;version="[2.4.0,2.5.0)",
org.apache.sshd.common.auth;version="[2.4.0,2.5.0)",
org.apache.sshd.common.config.keys;version="[2.4.0,2.5.0)",
+ org.apache.sshd.common.helpers;version="[2.4.0,2.5.0)",
org.apache.sshd.common.keyprovider;version="[2.4.0,2.5.0)",
org.apache.sshd.common.session;version="[2.4.0,2.5.0)",
org.apache.sshd.common.util.net;version="[2.4.0,2.5.0)",
org.apache.sshd.common.util.security;version="[2.4.0,2.5.0)",
org.apache.sshd.server;version="[2.4.0,2.5.0)",
+ org.apache.sshd.server.forward;version="[2.4.0,2.5.0)",
org.eclipse.jgit.api;version="[5.10.0,5.11.0)",
org.eclipse.jgit.api.errors;version="[5.10.0,5.11.0)",
org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.10.0,5.11.0)",
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
index 651ae7dec1..3427da667d 100644
--- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 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
@@ -11,19 +11,39 @@ package org.eclipse.jgit.transport.sshd;
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.PropertyResolverUtils;
+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.session.Session;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.server.ServerAuthenticationManager;
import org.apache.sshd.server.ServerFactoryManager;
+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;
@@ -211,4 +231,380 @@ public class ApacheSshTest extends SshTestBase {
git.fetch().call();
}
}
+
+ /**
+ * 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 = SshServer.setUpDefaultServer();
+ // Give the server its own host key
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ KeyPair proxyHostKey = generator.generateKeyPair();
+ proxy.setKeyPairProvider(
+ session -> Collections.singletonList(proxyHostKey));
+ // Allow (only) publickey authentication
+ proxy.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);
+ proxy.setPublickeyAuthenticator(
+ (userName, publicKey, session) -> user.equals(userName)
+ && KeyUtils.compareKeys(userProxyKey, publicKey));
+ // 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();
+ // 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",
+ proxy.getPort());
+ writer.append(',');
+ KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
+ proxy.getPort());
+ writer.append(' ');
+ PublicKeyEntry.appendPublicKeyEntry(writer,
+ proxyHostKey.getPublic());
+ writer.append('\n');
+ }
+ 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 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();
+ }
+ }
+ }
}