From 74e4bbe4c6f47980dfad1028da6cc5d0bd7ef326 Mon Sep 17 00:00:00 2001 From: Maros Marsalek Date: Tue, 7 Oct 2014 19:17:56 +0200 Subject: [PATCH] BUG-1612 Remove ganymed implementation of SSH server wrapper. Change-Id: Ib6342c74ff9335265d1fe7a12ca8caecbd72d94f Signed-off-by: Maros Marsalek --- .../netconf/ssh/NetconfSSHServer.java | 134 ------ .../ssh/authentication/PEMGenerator.java | 90 ---- .../netconf/ssh/threads/Handshaker.java | 415 ------------------ .../controller/netconf/netty/SSHTest.java | 85 ++-- .../ssh/authentication/SSHServerTest.java | 84 ++-- 5 files changed, 93 insertions(+), 715 deletions(-) delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java deleted file mode 100644 index 86206a7d5c..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. 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.controller.netconf.ssh; - -import com.google.common.base.Preconditions; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; - -import javax.annotation.concurrent.ThreadSafe; - -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.ssh.threads.Handshaker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; - -import io.netty.channel.EventLoopGroup; -import io.netty.channel.local.LocalAddress; - -/** - * Thread that accepts client connections. Accepted socket is forwarded to {@link org.opendaylight.controller.netconf.ssh.threads.Handshaker}, - * which is executed in {@link #handshakeExecutor}. - */ -@ThreadSafe -public final class NetconfSSHServer extends Thread implements AutoCloseable { - - private static final Logger logger = LoggerFactory.getLogger(NetconfSSHServer.class); - private static final AtomicLong sessionIdCounter = new AtomicLong(); - - private final ServerSocket serverSocket; - private final LocalAddress localAddress; - private final EventLoopGroup bossGroup; - private Optional authProvider = Optional.absent(); - private final ExecutorService handshakeExecutor; - private final char[] pem; - private volatile boolean up; - - private NetconfSSHServer(final int serverPort, final LocalAddress localAddress, final EventLoopGroup bossGroup, final char[] pem) throws IOException { - super(NetconfSSHServer.class.getSimpleName()); - this.bossGroup = bossGroup; - this.pem = pem; - logger.trace("Creating SSH server socket on port {}", serverPort); - this.serverSocket = new ServerSocket(serverPort); - if (serverSocket.isBound() == false) { - throw new IllegalStateException("Socket can't be bound to requested port :" + serverPort); - } - logger.trace("Server socket created."); - this.localAddress = localAddress; - this.up = true; - handshakeExecutor = Executors.newFixedThreadPool(10); - } - - public static NetconfSSHServer start(final int serverPort, final LocalAddress localAddress, final EventLoopGroup bossGroup, final char[] pemArray) throws IOException { - final NetconfSSHServer netconfSSHServer = new NetconfSSHServer(serverPort, localAddress, bossGroup, pemArray); - netconfSSHServer.start(); - return netconfSSHServer; - } - - public synchronized AuthProvider getAuthProvider() { - Preconditions.checkState(authProvider.isPresent(), "AuthenticationProvider is not set up, cannot authenticate user"); - return authProvider.get(); - } - - public synchronized void setAuthProvider(final AuthProvider authProvider) { - if(this.authProvider != null) { - logger.debug("Changing auth provider to {}", authProvider); - } - this.authProvider = Optional.fromNullable(authProvider); - } - - @Override - public void close() throws IOException { - up = false; - logger.trace("Closing SSH server socket."); - serverSocket.close(); - bossGroup.shutdownGracefully(); - logger.trace("SSH server socket closed."); - } - - @VisibleForTesting - public InetSocketAddress getLocalSocketAddress() { - return (InetSocketAddress) serverSocket.getLocalSocketAddress(); - } - - @Override - public void run() { - while (up) { - Socket acceptedSocket = null; - try { - acceptedSocket = serverSocket.accept(); - } catch (final IOException e) { - if (up == false) { - logger.trace("Exiting server thread", e); - } else { - logger.warn("Exception occurred during socket.accept", e); - } - } - if (acceptedSocket != null) { - try { - final Handshaker task = new Handshaker(acceptedSocket, localAddress, sessionIdCounter.incrementAndGet(), getAuthProvider(), bossGroup, pem); - handshakeExecutor.submit(task); - } catch (final IOException e) { - logger.warn("Cannot set PEMHostKey, closing connection", e); - closeSocket(acceptedSocket); - } catch (final IllegalStateException e) { - logger.warn("Cannot accept connection, closing", e); - closeSocket(acceptedSocket); - } - } - } - logger.debug("Server thread is exiting"); - } - - private void closeSocket(final Socket acceptedSocket) { - try { - acceptedSocket.close(); - } catch (final IOException e) { - logger.warn("Ignoring exception while closing socket", e); - } - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java deleted file mode 100644 index 53ab8219ee..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. 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.controller.netconf.ssh.authentication; - -import com.google.common.annotations.VisibleForTesting; -import java.io.FileInputStream; -import java.security.NoSuchAlgorithmException; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.bouncycastle.openssl.PEMWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.io.StringWriter; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; - -public class PEMGenerator { - private static final Logger logger = LoggerFactory.getLogger(PEMGenerator.class); - private static final int KEY_SIZE = 4096; - - - public static String readOrGeneratePK(File privateKeyFile) throws IOException { - if (privateKeyFile.exists() == false) { - // generate & save to file - try { - return generateTo(privateKeyFile); - } catch (Exception e) { - logger.error("Exception occurred while generating PEM string to {}", privateKeyFile, e); - throw new IllegalStateException("Error generating RSA key from file " + privateKeyFile); - } - } else { - // read from file - try (FileInputStream fis = new FileInputStream(privateKeyFile)) { - return IOUtils.toString(fis); - } catch (final IOException e) { - logger.error("Error reading RSA key from file {}", privateKeyFile, e); - throw new IOException("Error reading RSA key from file " + privateKeyFile, e); - } - } - } - - /** - * Generate private key to a file and return its content as string. - * - * @param privateFile path where private key should be generated - * @return String representation of private key - * @throws IOException - * @throws NoSuchAlgorithmException - */ - @VisibleForTesting - public static String generateTo(File privateFile) throws IOException, NoSuchAlgorithmException { - logger.info("Generating private key to {}", privateFile.getAbsolutePath()); - String privatePEM = generate(); - FileUtils.write(privateFile, privatePEM); - return privatePEM; - } - - @VisibleForTesting - public static String generate() throws NoSuchAlgorithmException, IOException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); - SecureRandom sr = new SecureRandom(); - keyGen.initialize(KEY_SIZE, sr); - KeyPair keypair = keyGen.generateKeyPair(); - return toString(keypair.getPrivate()); - } - - /** - * Get string representation of a key. - */ - private static String toString(Key key) throws IOException { - try (StringWriter writer = new StringWriter()) { - try (PEMWriter pemWriter = new PEMWriter(writer)) { - pemWriter.writeObject(key); - } - return writer.toString(); - } - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java deleted file mode 100644 index eec6c3a097..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. 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.controller.netconf.ssh.threads; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; - -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.util.messages.NetconfHelloMessageAdditionalHeader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.ethz.ssh2.AuthenticationResult; -import ch.ethz.ssh2.PtySettings; -import ch.ethz.ssh2.ServerAuthenticationCallback; -import ch.ethz.ssh2.ServerConnection; -import ch.ethz.ssh2.ServerConnectionCallback; -import ch.ethz.ssh2.ServerSession; -import ch.ethz.ssh2.ServerSessionCallback; -import ch.ethz.ssh2.SimpleServerSessionCallback; - -import com.google.common.base.Supplier; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufProcessor; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.local.LocalAddress; -import io.netty.channel.local.LocalChannel; -import io.netty.handler.stream.ChunkedStream; - -/** - * One instance represents per connection, responsible for ssh handshake. - * Once auth succeeds and correct subsystem is chosen, backend connection with - * netty netconf server is made. This task finishes right after negotiation is done. - */ -@ThreadSafe -public class Handshaker implements Runnable { - private static final Logger logger = LoggerFactory.getLogger(Handshaker.class); - - private final ServerConnection ganymedConnection; - private final String session; - - - public Handshaker(Socket socket, LocalAddress localAddress, long sessionId, AuthProvider authProvider, - EventLoopGroup bossGroup, final char[] pem) throws IOException { - - this.session = "Session " + sessionId; - - String remoteAddressWithPort = socket.getRemoteSocketAddress().toString().replace("/", ""); - logger.debug("{} started with {}", session, remoteAddressWithPort); - String remoteAddress, remotePort; - if (remoteAddressWithPort.contains(":")) { - String[] split = remoteAddressWithPort.split(":"); - remoteAddress = split[0]; - remotePort = split[1]; - } else { - remoteAddress = remoteAddressWithPort; - remotePort = ""; - } - ServerAuthenticationCallbackImpl serverAuthenticationCallback = new ServerAuthenticationCallbackImpl( - authProvider, session); - - ganymedConnection = new ServerConnection(socket); - - ServerConnectionCallbackImpl serverConnectionCallback = new ServerConnectionCallbackImpl( - serverAuthenticationCallback, remoteAddress, remotePort, session, - getGanymedAutoCloseable(ganymedConnection), localAddress, bossGroup); - - // initialize ganymed - ganymedConnection.setPEMHostKey(pem, null); - ganymedConnection.setAuthenticationCallback(serverAuthenticationCallback); - ganymedConnection.setServerConnectionCallback(serverConnectionCallback); - } - - - private static AutoCloseable getGanymedAutoCloseable(final ServerConnection ganymedConnection) { - return new AutoCloseable() { - @Override - public void close() throws Exception { - ganymedConnection.close(); - } - }; - } - - @Override - public void run() { - // let ganymed process handshake - logger.trace("{} is started", session); - try { - // TODO this should be guarded with a timer to prevent resource exhaustion - ganymedConnection.connect(); - } catch (IOException e) { - logger.debug("{} connection error", session, e); - } - logger.trace("{} is exiting", session); - } -} - -/** - * Netty client handler that forwards bytes from backed server to supplied output stream. - * When backend server closes the connection, remoteConnection.close() is called to tear - * down ssh connection. - */ -class SSHClientHandler extends ChannelInboundHandlerAdapter { - private static final Logger logger = LoggerFactory.getLogger(SSHClientHandler.class); - private final AutoCloseable remoteConnection; - private final BufferedOutputStream remoteOutputStream; - private final String session; - private ChannelHandlerContext channelHandlerContext; - - public SSHClientHandler(AutoCloseable remoteConnection, OutputStream remoteOutputStream, - String session) { - this.remoteConnection = remoteConnection; - this.remoteOutputStream = new BufferedOutputStream(remoteOutputStream); - this.session = session; - } - - @Override - public void channelActive(ChannelHandlerContext ctx) { - this.channelHandlerContext = ctx; - logger.debug("{} Client active", session); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws IOException { - ByteBuf bb = (ByteBuf) msg; - // we can block the server here so that slow client does not cause memory pressure - try { - bb.forEachByte(new ByteBufProcessor() { - @Override - public boolean process(byte value) throws Exception { - remoteOutputStream.write(value); - return true; - } - }); - } finally { - bb.release(); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws IOException { - logger.trace("{} Flushing", session); - remoteOutputStream.flush(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - // Close the connection when an exception is raised. - logger.warn("{} Unexpected exception from downstream", session, cause); - ctx.close(); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - logger.trace("{} channelInactive() called, closing remote client ctx", session); - remoteConnection.close();//this should close socket and all threads created for this client - this.channelHandlerContext = null; - } - - public ChannelHandlerContext getChannelHandlerContext() { - return checkNotNull(channelHandlerContext, "Channel is not active"); - } -} - -/** - * Ganymed handler that gets unencrypted input and output streams, connects them to netty. - * Checks that 'netconf' subsystem is chosen by user. - * Launches new ClientInputStreamPoolingThread thread once session is established. - * Writes custom header to netty server, to inform it about IP address and username. - */ -class ServerConnectionCallbackImpl implements ServerConnectionCallback { - private static final Logger logger = LoggerFactory.getLogger(ServerConnectionCallbackImpl.class); - public static final String NETCONF_SUBSYSTEM = "netconf"; - - private final Supplier currentUserSupplier; - private final String remoteAddress; - private final String remotePort; - private final String session; - private final AutoCloseable ganymedConnection; - private final LocalAddress localAddress; - private final EventLoopGroup bossGroup; - - ServerConnectionCallbackImpl(Supplier currentUserSupplier, String remoteAddress, String remotePort, String session, - AutoCloseable ganymedConnection, LocalAddress localAddress, EventLoopGroup bossGroup) { - this.currentUserSupplier = currentUserSupplier; - this.remoteAddress = remoteAddress; - this.remotePort = remotePort; - this.session = session; - this.ganymedConnection = ganymedConnection; - // initialize netty local connection - this.localAddress = localAddress; - this.bossGroup = bossGroup; - } - - private static ChannelFuture initializeNettyConnection(LocalAddress localAddress, EventLoopGroup bossGroup, - final SSHClientHandler sshClientHandler) { - Bootstrap clientBootstrap = new Bootstrap(); - clientBootstrap.group(bossGroup).channel(LocalChannel.class); - - clientBootstrap.handler(new ChannelInitializer() { - @Override - public void initChannel(LocalChannel ch) throws Exception { - ch.pipeline().addLast(sshClientHandler); - } - }); - // asynchronously initialize local connection to netconf server - return clientBootstrap.connect(localAddress); - } - - @Override - public ServerSessionCallback acceptSession(final ServerSession serverSession) { - String currentUser = currentUserSupplier.get(); - final String additionalHeader = new NetconfHelloMessageAdditionalHeader(currentUser, remoteAddress, - remotePort, "ssh", "client").toFormattedString(); - - - return new SimpleServerSessionCallback() { - @Override - public Runnable requestSubsystem(final ServerSession ss, final String subsystem) throws IOException { - return new Runnable() { - @Override - public void run() { - if (NETCONF_SUBSYSTEM.equals(subsystem)) { - // connect - final SSHClientHandler sshClientHandler = new SSHClientHandler(ganymedConnection, ss.getStdin(), session); - ChannelFuture clientChannelFuture = initializeNettyConnection(localAddress, bossGroup, sshClientHandler); - // get channel - final Channel channel = clientChannelFuture.awaitUninterruptibly().channel(); - - // write additional header before polling thread is started - // polling thread could process and forward data before additional header is written - // This will result into unexpected state: hello message without additional header and the next message with additional header - channel.writeAndFlush(Unpooled.copiedBuffer(additionalHeader.getBytes())); - - new ClientInputStreamPoolingThread(session, ss.getStdout(), channel, new AutoCloseable() { - @Override - public void close() throws Exception { - logger.trace("Closing both ganymed and local connection"); - try { - ganymedConnection.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing ganymed", e); - } - try { - channel.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing channel", e); - } - } - }, sshClientHandler.getChannelHandlerContext()).start(); - } else { - logger.debug("{} Wrong subsystem requested:'{}', closing ssh session", serverSession, subsystem); - String reason = "Only netconf subsystem is supported, requested:" + subsystem; - closeSession(ss, reason); - } - } - }; - } - - public void closeSession(ServerSession ss, String reason) { - logger.trace("{} Closing session - {}", serverSession, reason); - try { - ss.getStdin().write(reason.getBytes()); - } catch (IOException e) { - logger.warn("{} Exception while closing session", serverSession, e); - } - ss.close(); - } - - @Override - public Runnable requestPtyReq(final ServerSession ss, final PtySettings pty) throws IOException { - return new Runnable() { - @Override - public void run() { - closeSession(ss, "PTY request not supported"); - } - }; - } - - @Override - public Runnable requestShell(final ServerSession ss) throws IOException { - return new Runnable() { - @Override - public void run() { - closeSession(ss, "Shell not supported"); - } - }; - } - }; - } -} - -/** - * Only thread that is required during ssh session, forwards client's input to netty. - * When user closes connection, onEndOfInput.close() is called to tear down the local channel. - */ -class ClientInputStreamPoolingThread extends Thread { - private static final Logger logger = LoggerFactory.getLogger(ClientInputStreamPoolingThread.class); - - private final InputStream fromClientIS; - private final Channel serverChannel; - private final AutoCloseable onEndOfInput; - private final ChannelHandlerContext channelHandlerContext; - - ClientInputStreamPoolingThread(String session, InputStream fromClientIS, Channel serverChannel, AutoCloseable onEndOfInput, - ChannelHandlerContext channelHandlerContext) { - super(ClientInputStreamPoolingThread.class.getSimpleName() + " " + session); - this.fromClientIS = fromClientIS; - this.serverChannel = serverChannel; - this.onEndOfInput = onEndOfInput; - this.channelHandlerContext = channelHandlerContext; - } - - @Override - public void run() { - ChunkedStream chunkedStream = new ChunkedStream(fromClientIS); - try { - ByteBuf byteBuf; - while ((byteBuf = chunkedStream.readChunk(channelHandlerContext/*only needed for ByteBuf alloc */)) != null) { - serverChannel.writeAndFlush(byteBuf); - } - } catch (Exception e) { - logger.warn("Exception", e); - } finally { - logger.trace("End of input"); - // tear down connection - try { - onEndOfInput.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing socket", e); - } - } - } -} - -/** - * Authentication handler for ganymed. - * Provides current user name after authenticating using supplied AuthProvider. - */ -@NotThreadSafe -class ServerAuthenticationCallbackImpl implements ServerAuthenticationCallback, Supplier { - private static final Logger logger = LoggerFactory.getLogger(ServerAuthenticationCallbackImpl.class); - private final AuthProvider authProvider; - private final String session; - private String currentUser; - - ServerAuthenticationCallbackImpl(AuthProvider authProvider, String session) { - this.authProvider = authProvider; - this.session = session; - } - - @Override - public String initAuthentication(ServerConnection sc) { - logger.trace("{} Established connection", session); - return "Established connection" + "\r\n"; - } - - @Override - public String[] getRemainingAuthMethods(ServerConnection sc) { - return new String[]{ServerAuthenticationCallback.METHOD_PASSWORD}; - } - - @Override - public AuthenticationResult authenticateWithNone(ServerConnection sc, String username) { - return AuthenticationResult.FAILURE; - } - - @Override - public AuthenticationResult authenticateWithPassword(ServerConnection sc, String username, String password) { - checkState(currentUser == null); - try { - if (authProvider.authenticated(username, password)) { - currentUser = username; - logger.trace("{} user {} authenticated", session, currentUser); - return AuthenticationResult.SUCCESS; - } - } catch (Exception e) { - logger.warn("{} Authentication failed", session, e); - } - return AuthenticationResult.FAILURE; - } - - @Override - public AuthenticationResult authenticateWithPublicKey(ServerConnection sc, String username, String algorithm, - byte[] publicKey, byte[] signature) { - return AuthenticationResult.FAILURE; - } - - @Override - public String get() { - return currentUser; - } -} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java index eb2b644cbc..62ce587237 100644 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java @@ -11,9 +11,6 @@ package org.opendaylight.controller.netconf.netty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import com.google.common.base.Stopwatch; import io.netty.bootstrap.Bootstrap; @@ -23,16 +20,21 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; -import org.opendaylight.controller.netconf.auth.AuthProvider; import org.opendaylight.controller.netconf.netty.EchoClientHandler.State; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.LoginPassword; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.client.AsyncSshHandler; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,68 +42,77 @@ import org.slf4j.LoggerFactory; public class SSHTest { public static final Logger logger = LoggerFactory.getLogger(SSHTest.class); public static final String AHOJ = "ahoj\n"; - private EventLoopGroup nettyGroup; - HashedWheelTimer hashedWheelTimer; - @Before - public void setUp() throws Exception { + private static EventLoopGroup nettyGroup; + private static HashedWheelTimer hashedWheelTimer; + private static ExecutorService nioExec; + private static ScheduledExecutorService minaTimerEx; + + @BeforeClass + public static void setUp() throws Exception { hashedWheelTimer = new HashedWheelTimer(); nettyGroup = new NioEventLoopGroup(); + nioExec = Executors.newFixedThreadPool(1); + minaTimerEx = Executors.newScheduledThreadPool(1); } - @After - public void tearDown() throws Exception { + @AfterClass + public static void tearDown() throws Exception { hashedWheelTimer.stop(); - nettyGroup.shutdownGracefully(); + nettyGroup.shutdownGracefully().await(); + minaTimerEx.shutdownNow(); + nioExec.shutdownNow(); } @Test public void test() throws Exception { new Thread(new EchoServer(), "EchoServer").start(); - AuthProvider authProvider = mock(AuthProvider.class); - doReturn(true).when(authProvider).authenticated(anyString(), anyString()); - doReturn("auth").when(authProvider).toString(); - - NetconfSSHServer netconfSSHServer = NetconfSSHServer.start(10831, NetconfConfigUtil.getNetconfLocalAddress(), - new NioEventLoopGroup(), PEMGenerator.generate().toCharArray()); - netconfSSHServer.setAuthProvider(authProvider); - InetSocketAddress address = netconfSSHServer.getLocalSocketAddress(); + final InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 10831); + final SshProxyServer sshProxyServer = new SshProxyServer(minaTimerEx, nettyGroup, nioExec); + sshProxyServer.bind(addr, NetconfConfigUtil.getNetconfLocalAddress(), + new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return true; + } + }, new PEMGeneratorHostKeyProvider(Files.createTempFile("prefix", "suffix").toAbsolutePath().toString())); - final EchoClientHandler echoClientHandler = connectClient(new InetSocketAddress("localhost", address.getPort())); + final EchoClientHandler echoClientHandler = connectClient(addr); Stopwatch stopwatch = new Stopwatch().start(); - while(echoClientHandler.isConnected() == false && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(100); + while(echoClientHandler.isConnected() == false && stopwatch.elapsed(TimeUnit.SECONDS) < 30) { + Thread.sleep(500); } assertTrue(echoClientHandler.isConnected()); logger.info("connected, writing to client"); echoClientHandler.write(AHOJ); + // check that server sent back the same string stopwatch = stopwatch.reset().start(); - while (echoClientHandler.read().endsWith(AHOJ) == false && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(100); + while (echoClientHandler.read().endsWith(AHOJ) == false && stopwatch.elapsed(TimeUnit.SECONDS) < 30) { + Thread.sleep(500); } + try { - String read = echoClientHandler.read(); + final String read = echoClientHandler.read(); assertTrue(read + " should end with " + AHOJ, read.endsWith(AHOJ)); } finally { logger.info("Closing socket"); - netconfSSHServer.close(); - netconfSSHServer.join(); + sshProxyServer.close(); } } - public EchoClientHandler connectClient(InetSocketAddress address) { + public EchoClientHandler connectClient(final InetSocketAddress address) { final EchoClientHandler echoClientHandler = new EchoClientHandler(); - ChannelInitializer channelInitializer = new ChannelInitializer() { + final ChannelInitializer channelInitializer = new ChannelInitializer() { @Override - public void initChannel(NioSocketChannel ch) throws Exception { + public void initChannel(final NioSocketChannel ch) throws Exception { ch.pipeline().addFirst(AsyncSshHandler.createForNetconfSubsystem(new LoginPassword("a", "a"))); ch.pipeline().addLast(echoClientHandler); } }; - Bootstrap b = new Bootstrap(); + final Bootstrap b = new Bootstrap(); b.group(nettyGroup) .channel(NioSocketChannel.class) @@ -114,9 +125,9 @@ public class SSHTest { @Test public void testClientWithoutServer() throws Exception { - InetSocketAddress address = new InetSocketAddress(12345); + final InetSocketAddress address = new InetSocketAddress(12345); final EchoClientHandler echoClientHandler = connectClient(address); - Stopwatch stopwatch = new Stopwatch().start(); + final Stopwatch stopwatch = new Stopwatch().start(); while(echoClientHandler.getState() == State.CONNECTING && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { Thread.sleep(100); } diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java index 1151abcdf2..9cd0c9bcea 100644 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java @@ -12,19 +12,26 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import ch.ethz.ssh2.Connection; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import java.io.InputStream; import java.net.InetSocketAddress; -import junit.framework.Assert; -import org.apache.commons.io.IOUtils; +import java.nio.file.Files; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceListener; @@ -39,13 +46,15 @@ public class SSHServerTest { private static final String PASSWORD = "netconf"; private static final String HOST = "127.0.0.1"; private static final int PORT = 1830; - private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 8383); private static final Logger logger = LoggerFactory.getLogger(SSHServerTest.class); - private Thread sshServerThread; + + private SshProxyServer server; @Mock private BundleContext mockedContext; - + private final ExecutorService nioExec = Executors.newFixedThreadPool(1); + private final EventLoopGroup clientGroup = new NioEventLoopGroup(); + private final ScheduledExecutorService minaTimerEx = Executors.newScheduledThreadPool(1); @Before public void setUp() throws Exception { @@ -55,42 +64,39 @@ public class SSHServerTest { doReturn(new ServiceReference[0]).when(mockedContext).getServiceReferences(anyString(), anyString()); logger.info("Creating SSH server"); - String pem; - try (InputStream is = getClass().getResourceAsStream("/RSA.pk")) { - pem = IOUtils.toString(is); - } - - EventLoopGroup bossGroup = new NioEventLoopGroup(); - NetconfSSHServer server = NetconfSSHServer.start(PORT, NetconfConfigUtil.getNetconfLocalAddress(), - bossGroup, pem.toCharArray()); - server.setAuthProvider(new AuthProvider() { - @Override - public boolean authenticated(final String username, final String password) { - return true; - } - }); - - sshServerThread = new Thread(server); - sshServerThread.setDaemon(true); - sshServerThread.start(); - logger.info("SSH server on " + PORT); + final InetSocketAddress addr = InetSocketAddress.createUnresolved(HOST, PORT); + server = new SshProxyServer(minaTimerEx, clientGroup, nioExec); + server.bind(addr, NetconfConfigUtil.getNetconfLocalAddress(), + new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return true; + } + }, new PEMGeneratorHostKeyProvider(Files.createTempFile("prefix", "suffix").toAbsolutePath().toString())); + logger.info("SSH server started on " + PORT); } @Test - public void connect() { + public void connect() throws Exception { + final SshClient sshClient = SshClient.setUpDefaultClient(); + sshClient.start(); try { - Connection conn = new Connection(HOST, PORT); - Assert.assertNotNull(conn); - logger.info("connecting to SSH server"); - conn.connect(); - logger.info("authenticating ..."); - boolean isAuthenticated = conn.authenticateWithPassword(USER, PASSWORD); - Assert.assertTrue(isAuthenticated); - } catch (Exception e) { - logger.error("Error while starting SSH server.", e); + final ConnectFuture connect = sshClient.connect(USER, HOST, PORT); + connect.await(30, TimeUnit.SECONDS); + org.junit.Assert.assertTrue(connect.isConnected()); + final ClientSession session = connect.getSession(); + session.addPasswordIdentity(PASSWORD); + final AuthFuture auth = session.auth(); + auth.await(30, TimeUnit.SECONDS); + org.junit.Assert.assertTrue(auth.isSuccess()); + } finally { + sshClient.close(true); + server.close(); + clientGroup.shutdownGracefully().await(); + minaTimerEx.shutdownNow(); + nioExec.shutdownNow(); } - } } -- 2.36.6