*/
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;
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;
--- /dev/null
+/*
+ * 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;
+}
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;
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;
private ServerSocket socket;
@BeforeAll
- static void beforeAll() throws Exception {
+ static void beforeAll() {
group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
}
}
}
- 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());