External service integration support for ssh transport. 85/106785/6
authorRuslan Kashapov <[email protected]>
Mon, 3 Jul 2023 10:51:41 +0000 (13:51 +0300)
committerRobert Varga <[email protected]>
Tue, 25 Jul 2023 16:03:15 +0000 (18:03 +0200)
Existing authentication service expected to be used
for ssh authentication when server is running in karaf.

JIRA: NETCONF-590
Change-Id: I1c6df46ee165a78f7bdd87bd7597ae8f35f75715
Signed-off-by: Ruslan Kashapov <[email protected]>
Signed-off-by: Robert Varga <[email protected]>
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/SSHServer.java
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/ServerFactoryManagerConfigurator.java [new file with mode: 0644]
transport/transport-ssh/src/test/java/org/opendaylight/netconf/transport/ssh/SshClientServerTest.java

index f4adf2c70de04ca6a14dccadee2038baa8bff5bd..b728ec24ae90c27ec589c0def07f2cf3f81b5657 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.netconf.transport.ssh;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
@@ -67,28 +68,53 @@ public final class SSHServer extends SSHTransportStack {
     public static @NonNull ListenableFuture<SSHServer> connect(final TransportChannelListener listener,
             final Bootstrap bootstrap, final TcpClientGrouping connectParams, final SshServerGrouping serverParams)
             throws UnsupportedConfigurationException {
-        final var server = new SSHServer(listener, newFactoryManager(serverParams));
+        final var server = new SSHServer(listener, newFactoryManager(requireNonNull(serverParams), null));
         return transformUnderlay(server, TCPClient.connect(server.asListener(), bootstrap, connectParams));
     }
 
     public static @NonNull ListenableFuture<SSHServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping connectParams,
-            final SshServerGrouping serverParams)
-            throws UnsupportedConfigurationException {
-        final var server = new SSHServer(listener, newFactoryManager(serverParams));
-        return transformUnderlay(server, TCPServer.listen(server.asListener(), bootstrap, connectParams));
+            final SshServerGrouping serverParams) throws UnsupportedConfigurationException {
+        requireNonNull(serverParams);
+        return listen(listener, bootstrap, connectParams, serverParams, null);
     }
 
-    private static ServerFactoryManager newFactoryManager(
-            final SshServerGrouping serverParams)
+    /**
+     * Builds and starts SSH Server.
+     *
+     * @param listener server channel listener, required
+     * @param bootstrap server bootstrap instance, required
+     * @param connectParams tcp transport configuration, required
+     * @param serverParams ssh overlay configuration, optional if configurator is defined, required otherwise
+     * @param configurator server factory manager configurator, optional if serverParams is defined, required otherwise
+     * @return server instance as listenable future
+     * @throws UnsupportedConfigurationException if any of configurations is invalid or incomplete
+     * @throws NullPointerException if any of required parameters is null
+     * @throws IllegalArgumentException if both configurator and serverParams are null
+     */
+    public static @NonNull ListenableFuture<SSHServer> listen(final TransportChannelListener listener,
+            final ServerBootstrap bootstrap, final TcpServerGrouping connectParams,
+            final SshServerGrouping serverParams, final ServerFactoryManagerConfigurator configurator)
             throws UnsupportedConfigurationException {
-        var factoryMgr = SshServer.setUpDefaultServer();
-
-        ConfigUtils.setTransportParams(factoryMgr, serverParams.getTransportParams());
-        ConfigUtils.setKeepAlives(factoryMgr, serverParams.getKeepalives());
-        setServerIdentity(factoryMgr, serverParams.getServerIdentity());
-        setClientAuthentication(factoryMgr, serverParams.getClientAuthentication());
+        checkArgument(serverParams != null || configurator != null,
+            "Neither server parameters nor factory configurator is defined");
+        final var factoryMgr = newFactoryManager(serverParams, configurator);
+        final var server = new SSHServer(listener, factoryMgr);
+        return transformUnderlay(server, TCPServer.listen(server.asListener(), bootstrap, connectParams));
+    }
 
+    private static ServerFactoryManager newFactoryManager(final @Nullable SshServerGrouping serverParams,
+            final @Nullable ServerFactoryManagerConfigurator configurator) throws UnsupportedConfigurationException {
+        final var factoryMgr = SshServer.setUpDefaultServer();
+        if (serverParams != null) {
+            ConfigUtils.setTransportParams(factoryMgr, serverParams.getTransportParams());
+            ConfigUtils.setKeepAlives(factoryMgr, serverParams.getKeepalives());
+            setServerIdentity(factoryMgr, serverParams.getServerIdentity());
+            setClientAuthentication(factoryMgr, serverParams.getClientAuthentication());
+        }
+        if (configurator != null) {
+            configurator.configureServerFactoryManager(factoryMgr);
+        }
         factoryMgr.setServiceFactories(SshServer.DEFAULT_SERVICE_FACTORIES);
         factoryMgr.setScheduledExecutorService(ThreadUtils.newSingleThreadScheduledExecutor(""));
         return factoryMgr;
diff --git a/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/ServerFactoryManagerConfigurator.java b/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/ServerFactoryManagerConfigurator.java
new file mode 100644 (file)
index 0000000..5f8013a
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.ssh;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.shaded.sshd.server.ServerFactoryManager;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+
+/**
+ * Extension interface allowing one to customize {@link ServerFactoryManager} before it is used to create the
+ * {@link SSHServer} instance.
+ */
+@FunctionalInterface
+public interface ServerFactoryManagerConfigurator {
+    /**
+     * Apply custom configuration.
+     *
+     * @param factoryManager server factory manager instance
+     * @throws UnsupportedConfigurationException if the configuration is not acceptable
+     */
+    void configureServerFactoryManager(@NonNull ServerFactoryManager factoryManager)
+        throws UnsupportedConfigurationException;
+}
index f6c3a53a673c158e3a7c8ea8d34372e178bbacd3..a6eece375e2c96aaf5df5fd444ff52db7aef1155 100644 (file)
@@ -41,6 +41,8 @@ import org.apache.commons.codec.digest.Crypt;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -51,6 +53,8 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
+import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
+import org.opendaylight.netconf.shaded.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 import org.opendaylight.netconf.shaded.sshd.server.session.ServerSession;
 import org.opendaylight.netconf.transport.api.TransportChannel;
 import org.opendaylight.netconf.transport.api.TransportChannelListener;
@@ -100,7 +104,7 @@ public class SshClientServerTest {
     private ServerSocket socket;
 
     @BeforeAll
-    static void beforeAll() throws Exception {
+    static void beforeAll() {
         group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
     }
 
@@ -245,16 +249,58 @@ public class SshClientServerTest {
         }
     }
 
-    private static Channel assertChannel(List<TransportChannel> transportChannels) {
+    @Test
+    @DisplayName("SSH server with external initializer")
+    void externalServerInitializer() throws Exception {
+        final var username = getUsernameAndUpdate();
+        when(sshClientConfig.getClientIdentity()).thenReturn(buildClientIdentityWithPassword(username, PASSWORD));
+        // Accept all keys
+        when(sshClientConfig.getServerAuthentication()).thenReturn(null);
+
+        final var server = SSHServer.listen(serverListener,
+            NettyTransportSupport.newServerBootstrap().group(group),
+            tcpServerConfig, null, factoryManager -> {
+                // authenticate user by credentials and generate host key
+                factoryManager.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
+                factoryManager.setPasswordAuthenticator(
+                    (usr, psw, session) -> username.equals(usr) && PASSWORD.equals(psw));
+                factoryManager.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
+            }).get(2, TimeUnit.SECONDS);
+        try {
+            final var client = SSHClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
+                tcpClientConfig, sshClientConfig).get(2, TimeUnit.SECONDS);
+            try {
+                verify(serverListener, timeout(10_000))
+                    .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
+                verify(clientListener, timeout(10_000))
+                    .onTransportChannelEstablished(clientTransportChannelCaptor.capture());
+                // validate channels are in expected state
+                var serverChannel = assertChannel(serverTransportChannelCaptor.getAllValues());
+                var clientChannel = assertChannel(clientTransportChannelCaptor.getAllValues());
+                // validate channels are connecting same sockets
+                assertEquals(serverChannel.remoteAddress(), clientChannel.localAddress());
+                assertEquals(serverChannel.localAddress(), clientChannel.remoteAddress());
+                // validate sessions are authenticated
+                assertSession(ServerSession.class, server.getSessions());
+                assertSession(ClientSession.class, client.getSessions());
+            } finally {
+                client.shutdown().get(2, TimeUnit.SECONDS);
+            }
+        } finally {
+            server.shutdown().get(2, TimeUnit.SECONDS);
+        }
+    }
+
+    private static Channel assertChannel(final List<TransportChannel> transportChannels) {
         assertNotNull(transportChannels);
         assertEquals(1, transportChannels.size());
         final var channel = assertInstanceOf(SSHTransportChannel.class, transportChannels.get(0)).channel();
         assertNotNull(channel);
-        assertTrue(channel.isOpen()); // connection is open
+        assertTrue(channel.isOpen());
         return channel;
     }
 
-    private static <T extends Session> void assertSession(Class<T> type, Collection<Session> sessions) {
+    private static <T extends Session> void assertSession(final Class<T> type, final Collection<Session> sessions) {
         assertNotNull(sessions);
         assertEquals(1, sessions.size());
         final T session = assertInstanceOf(type, sessions.iterator().next());