From 8dd063e01eda1ce8f8c3cf0b49af17d6dfff388e Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Tue, 27 May 2014 12:41:16 +0200 Subject: [PATCH] BUG 624 - Make netconf TCP port optional. Previously netconf-impl opened a TCP port with no authentication on localhost, and netconf-ssh used it as a bridge to forward trafic after processing authentication and encryption. This patch creates new project netconf-tcp and modifies netconf-impl to open the netconf server on LocalAddress. Both tcp and ssh modules now communicate with this local server. Config ini is modified so that the TCP port (8383) is not enabled by default. Change-Id: I74bded660f10b20d09535d32308aff5b2ae611d9 Signed-off-by: Tomas Olvecky --- opendaylight/commons/opendaylight/pom.xml | 5 + .../framework/AbstractDispatcher.java | 44 +- .../distribution/opendaylight/pom.xml | 4 + .../main/resources/configuration/config.ini | 8 +- .../client/SshClientChannelInitializer.java | 6 +- .../client/TcpClientChannelInitializer.java | 9 +- .../client/test/TestingNetconfClient.java | 46 +- opendaylight/netconf/netconf-impl/pom.xml | 2 + .../netconf/impl/NetconfServerDispatcher.java | 20 +- .../impl/osgi/NetconfImplActivator.java | 37 +- .../netconf/it/NetconfITSecureTest.java | 28 +- .../controller/netconf/it/NetconfITTest.java | 30 +- .../nettyutil/AbstractChannelInitializer.java | 10 +- opendaylight/netconf/netconf-ssh/pom.xml | 26 +- .../netconf/ssh/NetconfSSHServer.java | 105 +++-- .../ssh/authentication/AuthProvider.java | 72 +++- .../authentication/AuthProviderInterface.java | 19 - .../ssh/authentication/PEMGenerator.java | 50 ++- .../netconf/ssh/osgi/NetconfSSHActivator.java | 124 ++---- .../netconf/ssh/threads/Handshaker.java | 406 ++++++++++++++++++ .../netconf/ssh/threads/IOThread.java | 70 --- .../netconf/ssh/threads/SocketThread.java | 208 --------- .../controller/netconf/KeyGeneratorTest.java | 65 --- .../controller/netconf/SSHServerTest.java | 68 --- .../controller/netconf/netty/EchoClient.java | 67 +++ .../netconf/netty/EchoClientHandler.java | 62 +++ .../controller/netconf/netty/EchoServer.java | 84 ++++ .../netconf/netty/EchoServerHandler.java | 61 +++ .../controller/netconf/netty/ProxyServer.java | 83 ++++ .../netconf/netty/ProxyServerHandler.java | 121 ++++++ .../controller/netconf/netty/SSHTest.java | 39 ++ .../ssh/authentication/SSHServerTest.java | 91 ++++ .../src/test/resources/logback-test.xml | 13 + opendaylight/netconf/netconf-tcp/pom.xml | 63 +++ .../netconf/tcp/netty/ProxyServer.java | 56 +++ .../netconf/tcp/netty/ProxyServerHandler.java | 119 +++++ .../netconf/tcp/osgi/NetconfTCPActivator.java | 50 +++ opendaylight/netconf/netconf-util/pom.xml | 3 +- .../netconf/util/osgi/NetconfConfigUtil.java | 70 +-- opendaylight/netconf/pom.xml | 1 + 40 files changed, 1695 insertions(+), 750 deletions(-) delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProviderInterface.java create mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/IOThread.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/SocketThread.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/KeyGeneratorTest.java delete mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/SSHServerTest.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClient.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClientHandler.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServer.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServerHandler.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServer.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServerHandler.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java create mode 100644 opendaylight/netconf/netconf-ssh/src/test/resources/logback-test.xml create mode 100644 opendaylight/netconf/netconf-tcp/pom.xml create mode 100644 opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServer.java create mode 100644 opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServerHandler.java create mode 100644 opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/osgi/NetconfTCPActivator.java diff --git a/opendaylight/commons/opendaylight/pom.xml b/opendaylight/commons/opendaylight/pom.xml index c166f668cc..e88e39a61e 100644 --- a/opendaylight/commons/opendaylight/pom.xml +++ b/opendaylight/commons/opendaylight/pom.xml @@ -946,6 +946,11 @@ ${netconf.version} test-jar + + org.opendaylight.controller + netconf-tcp + ${netconf.version} + org.opendaylight.controller netconf-util diff --git a/opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/AbstractDispatcher.java b/opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/AbstractDispatcher.java index 916ef9a88b..fef2c71969 100644 --- a/opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/AbstractDispatcher.java +++ b/opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/AbstractDispatcher.java @@ -7,12 +7,16 @@ */ package org.opendaylight.protocol.framework; +import com.google.common.base.Preconditions; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.local.LocalServerChannel; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; @@ -21,22 +25,20 @@ import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.Promise; - import java.io.Closeable; import java.net.InetSocketAddress; - +import java.net.SocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Preconditions; - /** * Dispatcher class for creating servers and clients. The idea is to first create servers and clients and the run the * start method that will handle sockets in different thread. */ public abstract class AbstractDispatcher, L extends SessionListener> implements Closeable { - protected interface PipelineInitializer> { + + protected interface ChannelPipelineInitializer> { /** * Initializes channel by specifying the handlers in its pipeline. Handlers are protocol specific, therefore this * method needs to be implemented in protocol specific Dispatchers. @@ -44,7 +46,11 @@ public abstract class AbstractDispatcher, L extends * @param channel whose pipeline should be defined, also to be passed to {@link SessionNegotiatorFactory} * @param promise to be passed to {@link SessionNegotiatorFactory} */ - void initializeChannel(SocketChannel channel, Promise promise); + void initializeChannel(CH channel, Promise promise); + } + + protected interface PipelineInitializer> extends ChannelPipelineInitializer { + } @@ -76,25 +82,43 @@ public abstract class AbstractDispatcher, L extends * @return ChannelFuture representing the binding process */ protected ChannelFuture createServer(final InetSocketAddress address, final PipelineInitializer initializer) { + return createServer(address, NioServerSocketChannel.class, initializer); + } + + /** + * Creates server. Each server needs factories to pass their instances to client sessions. + * + * @param address address to which the server should be bound + * @param channelClass The {@link Class} which is used to create {@link Channel} instances from. + * @param initializer instance of PipelineInitializer used to initialize the channel pipeline + * + * @return ChannelFuture representing the binding process + */ + protected ChannelFuture createServer(SocketAddress address, Class channelClass, + final ChannelPipelineInitializer initializer) { final ServerBootstrap b = new ServerBootstrap(); - b.childHandler(new ChannelInitializer() { + b.childHandler(new ChannelInitializer() { @Override - protected void initChannel(final SocketChannel ch) { + protected void initChannel(final CH ch) { initializer.initializeChannel(ch, new DefaultPromise(executor)); } }); b.option(ChannelOption.SO_BACKLOG, 128); - b.childOption(ChannelOption.SO_KEEPALIVE, true); + if (LocalServerChannel.class.equals(channelClass) == false) { + // makes no sense for LocalServer and produces warning + b.childOption(ChannelOption.SO_KEEPALIVE, true); + } customizeBootstrap(b); if (b.group() == null) { b.group(bossGroup, workerGroup); } try { - b.channel(NioServerSocketChannel.class); + b.channel(channelClass); } catch (IllegalStateException e) { + // FIXME: if this is ok, document why LOG.trace("Not overriding channelFactory on bootstrap {}", b, e); } diff --git a/opendaylight/distribution/opendaylight/pom.xml b/opendaylight/distribution/opendaylight/pom.xml index 5b44bb7569..3802370aca 100644 --- a/opendaylight/distribution/opendaylight/pom.xml +++ b/opendaylight/distribution/opendaylight/pom.xml @@ -902,6 +902,10 @@ org.opendaylight.controller netconf-ssh + + org.opendaylight.controller + netconf-tcp + org.opendaylight.controller netconf-util diff --git a/opendaylight/distribution/opendaylight/src/main/resources/configuration/config.ini b/opendaylight/distribution/opendaylight/src/main/resources/configuration/config.ini index f15f8f7404..f05afbb346 100644 --- a/opendaylight/distribution/opendaylight/src/main/resources/configuration/config.ini +++ b/opendaylight/distribution/opendaylight/src/main/resources/configuration/config.ini @@ -14,13 +14,11 @@ osgi.bundles=\ # Netconf startup configuration -# Netconf tcp address:port is optional with default value 127.0.0.1:8383 +# Netconf tcp address:port is optional #netconf.tcp.address=127.0.0.1 -#netconf.tcp.port=8384 - -#netconf.tcp.client.address=127.0.0.1 -#netconf.tcp.client.port=8384 +#netconf.tcp.port=8383 +# Netconf tcp address:port is optional netconf.ssh.address=0.0.0.0 netconf.ssh.port=1830 netconf.ssh.pk.path = ./configuration/RSA.pk diff --git a/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/SshClientChannelInitializer.java b/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/SshClientChannelInitializer.java index 799674487f..829ac304bd 100644 --- a/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/SshClientChannelInitializer.java +++ b/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/SshClientChannelInitializer.java @@ -7,7 +7,7 @@ */ package org.opendaylight.controller.netconf.client; -import io.netty.channel.socket.SocketChannel; +import io.netty.channel.Channel; import io.netty.util.concurrent.Promise; import java.io.IOException; import org.opendaylight.controller.netconf.nettyutil.AbstractChannelInitializer; @@ -31,7 +31,7 @@ final class SshClientChannelInitializer extends AbstractChannelInitializer promise) { + public void initialize(final Channel ch, final Promise promise) { try { final Invoker invoker = Invoker.subsystem("netconf"); ch.pipeline().addFirst(new SshHandler(authenticationHandler, invoker)); @@ -42,7 +42,7 @@ final class SshClientChannelInitializer extends AbstractChannelInitializer promise) { ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(new SessionListenerFactory() { diff --git a/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/TcpClientChannelInitializer.java b/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/TcpClientChannelInitializer.java index 4a0a089fae..ee8f8baf01 100644 --- a/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/TcpClientChannelInitializer.java +++ b/opendaylight/netconf/netconf-client/src/main/java/org/opendaylight/controller/netconf/client/TcpClientChannelInitializer.java @@ -7,7 +7,7 @@ */ package org.opendaylight.controller.netconf.client; -import io.netty.channel.socket.SocketChannel; +import io.netty.channel.Channel; import io.netty.util.concurrent.Promise; import org.opendaylight.controller.netconf.nettyutil.AbstractChannelInitializer; import org.opendaylight.protocol.framework.SessionListenerFactory; @@ -24,12 +24,7 @@ class TcpClientChannelInitializer extends AbstractChannelInitializer promise) { - super.initialize(ch, promise); - } - - @Override - protected void initializeSessionNegotiator(final SocketChannel ch, final Promise promise) { + protected void initializeSessionNegotiator(final Channel ch, final Promise promise) { ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(new SessionListenerFactory() { @Override diff --git a/opendaylight/netconf/netconf-client/src/test/java/org/opendaylight/controller/netconf/client/test/TestingNetconfClient.java b/opendaylight/netconf/netconf-client/src/test/java/org/opendaylight/controller/netconf/client/test/TestingNetconfClient.java index 60d8f3044a..afa17532d5 100644 --- a/opendaylight/netconf/netconf-client/src/test/java/org/opendaylight/controller/netconf/client/test/TestingNetconfClient.java +++ b/opendaylight/netconf/netconf-client/src/test/java/org/opendaylight/controller/netconf/client/test/TestingNetconfClient.java @@ -8,24 +8,35 @@ package org.opendaylight.controller.netconf.client.test; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.HashedWheelTimer; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GlobalEventExecutor; import java.io.Closeable; import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; - import org.opendaylight.controller.netconf.api.NetconfMessage; import org.opendaylight.controller.netconf.client.NetconfClientDispatcher; +import org.opendaylight.controller.netconf.client.NetconfClientDispatcherImpl; import org.opendaylight.controller.netconf.client.NetconfClientSession; import org.opendaylight.controller.netconf.client.NetconfClientSessionListener; import org.opendaylight.controller.netconf.client.SimpleNetconfClientSessionListener; import org.opendaylight.controller.netconf.client.conf.NetconfClientConfiguration; - -import com.google.common.base.Preconditions; -import com.google.common.collect.Sets; -import io.netty.util.concurrent.Future; +import org.opendaylight.controller.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol; +import org.opendaylight.controller.netconf.client.conf.NetconfClientConfigurationBuilder; +import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler; +import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.LoginPassword; +import org.opendaylight.protocol.framework.NeverReconnectStrategy; /** @@ -95,4 +106,29 @@ public class TestingNetconfClient implements Closeable { Preconditions.checkState(clientSession != null, "Client was not initialized successfully"); return Sets.newHashSet(clientSession.getServerCapabilities()); } + + public static void main(String[] args) throws Exception { + HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(); + NioEventLoopGroup nettyGroup = new NioEventLoopGroup(); + NetconfClientDispatcherImpl netconfClientDispatcher = new NetconfClientDispatcherImpl(nettyGroup, nettyGroup, hashedWheelTimer); + LoginPassword authHandler = new LoginPassword("admin", "admin"); + TestingNetconfClient client = new TestingNetconfClient("client", netconfClientDispatcher, getClientConfig("127.0.0.1", 1830, true, Optional.of(authHandler))); + System.out.println(client.getCapabilities()); + } + + private static NetconfClientConfiguration getClientConfig(String host ,int port, boolean ssh, Optional maybeAuthHandler) throws UnknownHostException { + InetSocketAddress netconfAddress = new InetSocketAddress(InetAddress.getByName(host), port); + final NetconfClientConfigurationBuilder b = NetconfClientConfigurationBuilder.create(); + b.withAddress(netconfAddress); + b.withSessionListener(new SimpleNetconfClientSessionListener()); + b.withReconnectStrategy(new NeverReconnectStrategy(GlobalEventExecutor.INSTANCE, + NetconfClientConfigurationBuilder.DEFAULT_CONNECTION_TIMEOUT_MILLIS)); + if (ssh) { + b.withProtocol(NetconfClientProtocol.SSH); + b.withAuthHandler(maybeAuthHandler.get()); + } else { + b.withProtocol(NetconfClientProtocol.TCP); + } + return b.build(); + } } diff --git a/opendaylight/netconf/netconf-impl/pom.xml b/opendaylight/netconf/netconf-impl/pom.xml index 1d94517152..c60506ef44 100644 --- a/opendaylight/netconf/netconf-impl/pom.xml +++ b/opendaylight/netconf/netconf-impl/pom.xml @@ -109,6 +109,7 @@ org.apache.felix maven-bundle-plugin + 2.3.7 org.opendaylight.controller.netconf.impl.osgi.NetconfImplActivator @@ -121,6 +122,7 @@ io.netty.buffer, io.netty.handler.codec, io.netty.channel.nio, + io.netty.channel.local, javax.annotation, javax.management, javax.net.ssl, diff --git a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/NetconfServerDispatcher.java b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/NetconfServerDispatcher.java index de3dee1443..4dfb749818 100644 --- a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/NetconfServerDispatcher.java +++ b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/NetconfServerDispatcher.java @@ -8,8 +8,13 @@ package org.opendaylight.controller.netconf.impl; +import com.google.common.annotations.VisibleForTesting; +import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; import io.netty.channel.socket.SocketChannel; import io.netty.util.concurrent.Promise; import java.net.InetSocketAddress; @@ -27,6 +32,7 @@ public class NetconfServerDispatcher extends AbstractDispatcher() { @@ -37,6 +43,15 @@ public class NetconfServerDispatcher extends AbstractDispatcher() { + @Override + public void initializeChannel(final LocalChannel ch, final Promise promise) { + initializer.initialize(ch, promise); + } + }); + } + public static class ServerChannelInitializer extends AbstractChannelInitializer { public static final String DESERIALIZER_EX_HANDLER_KEY = "deserializerExHandler"; @@ -50,16 +65,15 @@ public class NetconfServerDispatcher extends AbstractDispatcher promise) { + protected void initializeSessionNegotiator(Channel ch, Promise promise) { ch.pipeline().addAfter(DESERIALIZER_EX_HANDLER_KEY, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(null, ch, promise)); } } - } diff --git a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/osgi/NetconfImplActivator.java b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/osgi/NetconfImplActivator.java index 7130dc3501..6ab62ef29a 100644 --- a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/osgi/NetconfImplActivator.java +++ b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/osgi/NetconfImplActivator.java @@ -7,12 +7,13 @@ */ package org.opendaylight.controller.netconf.impl.osgi; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.HashedWheelTimer; import java.lang.management.ManagementFactory; -import java.net.InetSocketAddress; import java.util.Dictionary; import java.util.Hashtable; import java.util.concurrent.TimeUnit; - import org.opendaylight.controller.netconf.api.monitoring.NetconfMonitoringService; import org.opendaylight.controller.netconf.impl.DefaultCommitNotificationProducer; import org.opendaylight.controller.netconf.impl.NetconfServerDispatcher; @@ -26,9 +27,6 @@ import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.util.HashedWheelTimer; - public class NetconfImplActivator implements BundleActivator { private static final Logger logger = LoggerFactory.getLogger(NetconfImplActivator.class); @@ -40,17 +38,16 @@ public class NetconfImplActivator implements BundleActivator { private ServiceRegistration regMonitoring; @Override - public void start(final BundleContext context) { - final InetSocketAddress address = NetconfConfigUtil.extractTCPNetconfServerAddress(context, - NetconfConfigUtil.DEFAULT_NETCONF_TCP_ADDRESS); - final NetconfOperationServiceFactoryListenerImpl factoriesListener = new NetconfOperationServiceFactoryListenerImpl(); + public void start(final BundleContext context) { + + NetconfOperationServiceFactoryListenerImpl factoriesListener = new NetconfOperationServiceFactoryListenerImpl(); startOperationServiceFactoryTracker(context, factoriesListener); - final SessionIdProvider idProvider = new SessionIdProvider(); + SessionIdProvider idProvider = new SessionIdProvider(); timer = new HashedWheelTimer(); - long connectionTimeoutMillis = NetconfConfigUtil.extractTimeoutMillis(context); + commitNot = new DefaultCommitNotificationProducer(ManagementFactory.getPlatformMBeanServer()); SessionMonitoringService monitoringService = startMonitoringService(context, factoriesListener); @@ -62,24 +59,24 @@ public class NetconfImplActivator implements BundleActivator { NetconfServerDispatcher.ServerChannelInitializer serverChannelInitializer = new NetconfServerDispatcher.ServerChannelInitializer( serverNegotiatorFactory); + NetconfServerDispatcher dispatch = new NetconfServerDispatcher(serverChannelInitializer, eventLoopGroup, eventLoopGroup); - NetconfServerDispatcher dispatch = new NetconfServerDispatcher(serverChannelInitializer, eventLoopGroup, - eventLoopGroup); - - logger.info("Starting TCP netconf server at {}", address); - dispatch.createServer(address); + LocalAddress address = NetconfConfigUtil.getNetconfLocalAddress(); + logger.trace("Starting local netconf server at {}", address); + dispatch.createLocalServer(address); context.registerService(NetconfOperationProvider.class, factoriesListener, null); + } - private void startOperationServiceFactoryTracker(final BundleContext context, final NetconfOperationServiceFactoryListenerImpl factoriesListener) { + private void startOperationServiceFactoryTracker(BundleContext context, NetconfOperationServiceFactoryListenerImpl factoriesListener) { factoriesTracker = new NetconfOperationServiceFactoryTracker(context, factoriesListener); factoriesTracker.open(); } - private NetconfMonitoringServiceImpl startMonitoringService(final BundleContext context, final NetconfOperationServiceFactoryListenerImpl factoriesListener) { - final NetconfMonitoringServiceImpl netconfMonitoringServiceImpl = new NetconfMonitoringServiceImpl(factoriesListener); - final Dictionary dic = new Hashtable<>(); + private NetconfMonitoringServiceImpl startMonitoringService(BundleContext context, NetconfOperationServiceFactoryListenerImpl factoriesListener) { + NetconfMonitoringServiceImpl netconfMonitoringServiceImpl = new NetconfMonitoringServiceImpl(factoriesListener); + Dictionary dic = new Hashtable<>(); regMonitoring = context.registerService(NetconfMonitoringService.class, netconfMonitoringServiceImpl, dic); return netconfMonitoringServiceImpl; diff --git a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java index 140284e4ee..0969bd92a5 100644 --- a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java +++ b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java @@ -16,13 +16,13 @@ import static org.mockito.Mockito.mock; import ch.ethz.ssh2.Connection; import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.GlobalEventExecutor; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; -import java.nio.file.Files; import java.util.Collection; import java.util.List; import junit.framework.Assert; @@ -50,16 +50,14 @@ import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; import org.opendaylight.controller.netconf.util.messages.NetconfMessageUtil; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.opendaylight.controller.netconf.util.test.XmlFileLoader; import org.opendaylight.controller.netconf.util.xml.XmlUtil; -import org.opendaylight.controller.sal.authorization.AuthResultEnum; -import org.opendaylight.controller.usermanager.IUserManager; import org.opendaylight.protocol.framework.NeverReconnectStrategy; public class NetconfITSecureTest extends AbstractNetconfConfigTest { private static final InetSocketAddress tlsAddress = new InetSocketAddress("127.0.0.1", 12024); - private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 12023); private DefaultCommitNotificationProducer commitNot; private NetconfSSHServer sshServer; @@ -79,13 +77,10 @@ public class NetconfITSecureTest extends AbstractNetconfConfigTest { final NetconfServerDispatcher dispatchS = createDispatcher(factoriesListener); - ChannelFuture s = dispatchS.createServer(tcpAddress); + ChannelFuture s = dispatchS.createLocalServer(NetconfConfigUtil.getNetconfLocalAddress()); s.await(); - - sshServer = NetconfSSHServer.start(tlsAddress.getPort(), tcpAddress, getAuthProvider()); - Thread thread = new Thread(sshServer); - thread.setDaemon(true); - thread.start(); + EventLoopGroup bossGroup = new NioEventLoopGroup(); + sshServer = NetconfSSHServer.start(tlsAddress.getPort(), NetconfConfigUtil.getNetconfLocalAddress(), getAuthProvider(), bossGroup); } private NetconfServerDispatcher createDispatcher(NetconfOperationServiceFactoryListenerImpl factoriesListener) { @@ -140,13 +135,10 @@ public class NetconfITSecureTest extends AbstractNetconfConfigTest { } public AuthProvider getAuthProvider() throws Exception { - final IUserManager userManager = mock(IUserManager.class); - doReturn(AuthResultEnum.AUTH_ACCEPT).when(userManager).authenticate(anyString(), anyString()); - - final File privateKeyFile = Files.createTempFile("tmp-netconf-test", "pk").toFile(); - privateKeyFile.deleteOnExit(); - String privateKeyPEMString = PEMGenerator.generateTo(privateKeyFile); - return new AuthProvider(userManager, privateKeyPEMString); + AuthProvider mock = mock(AuthProvider.class); + doReturn(true).when(mock).authenticated(anyString(), anyString()); + doReturn(PEMGenerator.generate().toCharArray()).when(mock).getPEMAsCharArray(); + return mock; } public AuthenticationHandler getAuthHandler() throws IOException { diff --git a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITTest.java b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITTest.java index fd43f67c05..60a5207daa 100644 --- a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITTest.java +++ b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITTest.java @@ -8,7 +8,6 @@ package org.opendaylight.controller.netconf.it; -import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; @@ -17,6 +16,10 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import io.netty.channel.ChannelFuture; import java.io.IOException; import java.io.InputStream; import java.lang.management.ManagementFactory; @@ -29,10 +32,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; - import javax.management.ObjectName; import javax.xml.parsers.ParserConfigurationException; - import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -66,32 +67,20 @@ import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controll import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.config.test.types.rev131127.TestIdentity2; import org.opendaylight.yangtools.yang.data.impl.codec.CodecRegistry; import org.opendaylight.yangtools.yang.data.impl.codec.IdentityCodec; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.xml.sax.SAXException; -import com.google.common.base.Throwables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import io.netty.channel.ChannelFuture; - public class NetconfITTest extends AbstractNetconfConfigTest { // TODO refactor, pull common code up to AbstractNetconfITTest - private static final Logger logger = LoggerFactory.getLogger(NetconfITTest.class); - private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 12023); - private static final InetSocketAddress sshAddress = new InetSocketAddress("127.0.0.1", 10830); - private static final String USERNAME = "netconf"; - private static final String PASSWORD = "netconf"; - private NetconfMessage getConfig, getConfigCandidate, editConfig, - closeSession, startExi, stopExi; + + private NetconfMessage getConfig, getConfigCandidate, editConfig, closeSession; private DefaultCommitNotificationProducer commitNot; private NetconfServerDispatcher dispatch; @@ -139,10 +128,6 @@ public class NetconfITTest extends AbstractNetconfConfigTest { this.editConfig = XmlFileLoader.xmlFileToNetconfMessage("netconfMessages/edit_config.xml"); this.getConfig = XmlFileLoader.xmlFileToNetconfMessage("netconfMessages/getConfig.xml"); this.getConfigCandidate = XmlFileLoader.xmlFileToNetconfMessage("netconfMessages/getConfig_candidate.xml"); - this.startExi = XmlFileLoader - .xmlFileToNetconfMessage("netconfMessages/startExi.xml"); - this.stopExi = XmlFileLoader - .xmlFileToNetconfMessage("netconfMessages/stopExi.xml"); this.closeSession = XmlFileLoader.xmlFileToNetconfMessage("netconfMessages/closeSession.xml"); } @@ -166,7 +151,7 @@ public class NetconfITTest extends AbstractNetconfConfigTest { yangDependencies.add(resourceAsStream); } } - assertEquals("Some yang files were not found", emptyList(), failedToFind); + assertEquals("Some yang files were not found", Collections.emptyList(), failedToFind); return yangDependencies; } @@ -198,6 +183,7 @@ public class NetconfITTest extends AbstractNetconfConfigTest { public void testTwoSessions() throws Exception { try (TestingNetconfClient netconfClient = new TestingNetconfClient("1", clientDispatcher, getClientConfiguration(tcpAddress, 10000))) { try (TestingNetconfClient netconfClient2 = new TestingNetconfClient("2", clientDispatcher, getClientConfiguration(tcpAddress, 10000))) { + assertNotNull(netconfClient2.getCapabilities()); } } } diff --git a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/AbstractChannelInitializer.java b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/AbstractChannelInitializer.java index e88bf53ae0..7897666ddc 100644 --- a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/AbstractChannelInitializer.java +++ b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/AbstractChannelInitializer.java @@ -8,7 +8,7 @@ package org.opendaylight.controller.netconf.nettyutil; -import io.netty.channel.socket.SocketChannel; +import io.netty.channel.Channel; import io.netty.util.concurrent.Promise; import org.opendaylight.controller.netconf.api.NetconfSession; import org.opendaylight.controller.netconf.nettyutil.handler.FramingMechanismHandlerFactory; @@ -25,7 +25,7 @@ public abstract class AbstractChannelInitializer { public static final String NETCONF_MESSAGE_FRAME_ENCODER = "frameEncoder"; public static final String NETCONF_SESSION_NEGOTIATOR = "negotiator"; - public void initialize(SocketChannel ch, Promise promise) { + public void initialize(Channel ch, Promise promise) { ch.pipeline().addLast(NETCONF_MESSAGE_AGGREGATOR, new NetconfEOMAggregator()); initializeMessageDecoder(ch); ch.pipeline().addLast(NETCONF_MESSAGE_FRAME_ENCODER, FramingMechanismHandlerFactory.createHandler(FramingMechanism.EOM)); @@ -34,13 +34,13 @@ public abstract class AbstractChannelInitializer { initializeSessionNegotiator(ch, promise); } - protected void initializeMessageEncoder(SocketChannel ch) { + protected void initializeMessageEncoder(Channel ch) { // Special encoding handler for hello message to include additional header if available, // it is thrown away after successful negotiation ch.pipeline().addLast(NETCONF_MESSAGE_ENCODER, new NetconfHelloMessageToXMLEncoder()); } - protected void initializeMessageDecoder(SocketChannel ch) { + protected void initializeMessageDecoder(Channel ch) { // Special decoding handler for hello message to parse additional header if available, // it is thrown away after successful negotiation ch.pipeline().addLast(NETCONF_MESSAGE_DECODER, new NetconfXMLToHelloMessageDecoder()); @@ -50,6 +50,6 @@ public abstract class AbstractChannelInitializer { * Insert session negotiator into the pipeline. It must be inserted after message decoder * identified by {@link AbstractChannelInitializer#NETCONF_MESSAGE_DECODER}, (or any other custom decoder processor) */ - protected abstract void initializeSessionNegotiator(SocketChannel ch, Promise promise); + protected abstract void initializeSessionNegotiator(Channel ch, Promise promise); } diff --git a/opendaylight/netconf/netconf-ssh/pom.xml b/opendaylight/netconf/netconf-ssh/pom.xml index 622881352e..cbd3efc57f 100644 --- a/opendaylight/netconf/netconf-ssh/pom.xml +++ b/opendaylight/netconf/netconf-ssh/pom.xml @@ -56,21 +56,25 @@ org.apache.felix maven-bundle-plugin + 2.3.7 org.opendaylight.controller.netconf.ssh.osgi.NetconfSSHActivator com.google.common.base, - ch.ethz.ssh2, - ch.ethz.ssh2.signature, - org.apache.commons.io, - org.opendaylight.controller.netconf.util.osgi, - org.opendaylight.controller.usermanager, - org.opendaylight.controller.sal.authorization, - org.opendaylight.controller.sal.utils, - org.osgi.framework, - org.osgi.util.tracker, - org.slf4j, - org.bouncycastle.openssl + ch.ethz.ssh2, + ch.ethz.ssh2.signature, + org.apache.commons.io, + org.opendaylight.controller.netconf.util.osgi, + org.opendaylight.controller.usermanager, + org.opendaylight.controller.sal.authorization, + org.opendaylight.controller.sal.utils, + org.osgi.framework, + org.osgi.util.tracker, + org.slf4j, + org.bouncycastle.openssl, + io.netty.bootstrap, io.netty.buffer, io.netty.channel, io.netty.channel.local, io.netty.channel.nio, + io.netty.handler.stream, io.netty.util.concurrent, org.apache.commons.lang3, + org.opendaylight.controller.netconf.util.messages 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 index c6974d4982..08bf9836b2 100644 --- 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 @@ -7,79 +7,94 @@ */ package org.opendaylight.controller.netconf.ssh; -import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; -import org.opendaylight.controller.netconf.ssh.threads.SocketThread; -import org.opendaylight.controller.usermanager.IUserManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.concurrent.ThreadSafe; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; 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.ssh.authentication.AuthProvider; +import org.opendaylight.controller.netconf.ssh.threads.Handshaker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * 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 implements Runnable { +public final class NetconfSSHServer extends Thread implements AutoCloseable { - private ServerSocket ss = null; - private static final Logger logger = LoggerFactory.getLogger(NetconfSSHServer.class); - private static final AtomicLong sesssionId = new AtomicLong(); - private final InetSocketAddress clientAddress; - private final AuthProvider authProvider; - private volatile boolean up = false; + private static final Logger logger = LoggerFactory.getLogger(NetconfSSHServer.class); + private static final AtomicLong sessionIdCounter = new AtomicLong(); - private NetconfSSHServer(int serverPort,InetSocketAddress clientAddress, AuthProvider authProvider) throws IllegalStateException, IOException { + private final ServerSocket serverSocket; + private final LocalAddress localAddress; + private final EventLoopGroup bossGroup; + private final AuthProvider authProvider; + private final ExecutorService handshakeExecutor; + private volatile boolean up; - logger.trace("Creating SSH server socket on port {}",serverPort); - this.ss = new ServerSocket(serverPort); - if (!ss.isBound()){ - throw new IllegalStateException("Socket can't be bound to requested port :"+serverPort); + private NetconfSSHServer(int serverPort, LocalAddress localAddress, AuthProvider authProvider, EventLoopGroup bossGroup) throws IOException { + super(NetconfSSHServer.class.getSimpleName()); + this.bossGroup = bossGroup; + 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.clientAddress = clientAddress; + this.localAddress = localAddress; this.authProvider = authProvider; this.up = true; + handshakeExecutor = Executors.newFixedThreadPool(10); } - public static NetconfSSHServer start(int serverPort, InetSocketAddress clientAddress,AuthProvider authProvider) throws IllegalStateException, IOException { - return new NetconfSSHServer(serverPort, clientAddress,authProvider); + public static NetconfSSHServer start(int serverPort, LocalAddress localAddress, AuthProvider authProvider, EventLoopGroup bossGroup) throws IOException { + NetconfSSHServer netconfSSHServer = new NetconfSSHServer(serverPort, localAddress, authProvider, bossGroup); + netconfSSHServer.start(); + return netconfSSHServer; } - public void stop() throws IOException { + @Override + public void close() throws IOException { up = false; logger.trace("Closing SSH server socket."); - ss.close(); + serverSocket.close(); + bossGroup.shutdownGracefully(); logger.trace("SSH server socket closed."); } - public void removeUserManagerService(){ - this.authProvider.removeUserManagerService(); - } - - public void addUserManagerService(IUserManager userManagerService){ - this.authProvider.addUserManagerService(userManagerService); - } - public boolean isUp(){ - return this.up; - } @Override public void run() { while (up) { - logger.trace("Starting new socket thread."); + Socket acceptedSocket = null; try { - SocketThread.start(ss.accept(), clientAddress, sesssionId.incrementAndGet(), authProvider); - } - catch (IOException e) { - if( up ) { - logger.error("Exception occurred during socket thread initialization", e); + acceptedSocket = serverSocket.accept(); + } catch (IOException e) { + if (up == false) { + logger.trace("Exiting server thread", e); + } else { + logger.warn("Exception occurred during socket.accept", e); } - else { - // We're shutting down so an exception is expected as the socket's been closed. - // Log to debug. - logger.debug("Shutting down - got expected exception: " + e); + } + if (acceptedSocket != null) { + try { + Handshaker task = new Handshaker(acceptedSocket, localAddress, sessionIdCounter.incrementAndGet(), authProvider, bossGroup); + handshakeExecutor.submit(task); + } catch (IOException e) { + logger.warn("Cannot set PEMHostKey, closing connection", e); + try { + acceptedSocket.close(); + } catch (IOException e1) { + logger.warn("Ignoring exception while closing socket", e); + } } } } + logger.debug("Server thread is exiting"); } } diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProvider.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProvider.java index 2e9a0b9d8b..5d39dd1eb8 100644 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProvider.java +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProvider.java @@ -7,41 +7,75 @@ */ package org.opendaylight.controller.netconf.ssh.authentication; -import java.io.IOException; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; import org.opendaylight.controller.sal.authorization.AuthResultEnum; import org.opendaylight.controller.usermanager.IUserManager; -import static com.google.common.base.Preconditions.checkNotNull; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class AuthProvider implements AuthProviderInterface { +public class AuthProvider { + private static final Logger logger = LoggerFactory.getLogger(AuthProvider.class); - private IUserManager um; private final String pem; + private IUserManager nullableUserManager; - public AuthProvider(IUserManager ium, String pemCertificate) throws IllegalArgumentException, IOException { + public AuthProvider(String pemCertificate, final BundleContext bundleContext) { checkNotNull(pemCertificate, "Parameter 'pemCertificate' is null"); - checkNotNull(ium, "No user manager service available."); - this.um = ium; pem = pemCertificate; + + ServiceTrackerCustomizer customizer = new ServiceTrackerCustomizer() { + @Override + public IUserManager addingService(final ServiceReference reference) { + logger.trace("Service {} added", reference); + nullableUserManager = bundleContext.getService(reference); + return nullableUserManager; + } + + @Override + public void modifiedService(final ServiceReference reference, final IUserManager service) { + logger.trace("Replacing modified service {} in netconf SSH.", reference); + nullableUserManager = service; + } + + @Override + public void removedService(final ServiceReference reference, final IUserManager service) { + logger.trace("Removing service {} from netconf SSH. " + + "SSH won't authenticate users until IUserManager service will be started.", reference); + synchronized (AuthProvider.this) { + nullableUserManager = null; + } + } + }; + ServiceTracker listenerTracker = new ServiceTracker<>(bundleContext, IUserManager.class, customizer); + listenerTracker.open(); } - @Override - public boolean authenticated(String username, String password) { - AuthResultEnum authResult = this.um.authenticate(username, password); + /** + * Authenticate user. This implementation tracks IUserManager and delegates the decision to it. If the service is not + * available, IllegalStateException is thrown. + */ + public synchronized boolean authenticated(String username, String password) { + if (nullableUserManager == null) { + logger.warn("Cannot authenticate user '{}', user manager service is missing", username); + throw new IllegalStateException("User manager service is not available"); + } + AuthResultEnum authResult = nullableUserManager.authenticate(username, password); + logger.debug("Authentication result for user '{}' : {}", username, authResult); return authResult.equals(AuthResultEnum.AUTH_ACCEPT) || authResult.equals(AuthResultEnum.AUTH_ACCEPT_LOC); } - @Override public char[] getPEMAsCharArray() { return pem.toCharArray(); } - @Override - public void removeUserManagerService() { - this.um = null; - } - - @Override - public void addUserManagerService(IUserManager userManagerService) { - this.um = userManagerService; + @VisibleForTesting + void setNullableUserManager(IUserManager nullableUserManager) { + this.nullableUserManager = nullableUserManager; } } diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProviderInterface.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProviderInterface.java deleted file mode 100644 index fad0f79a4e..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/AuthProviderInterface.java +++ /dev/null @@ -1,19 +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 org.opendaylight.controller.usermanager.IUserManager; - -public interface AuthProviderInterface { - - public boolean authenticated(String username, String password) throws IllegalStateException; - public char[] getPEMAsCharArray() throws Exception; - public void removeUserManagerService(); - public void addUserManagerService(IUserManager userManagerService); -} 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 index 348fe006f3..53ab8219ee 100644 --- 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 @@ -8,8 +8,11 @@ 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; @@ -26,17 +29,55 @@ 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(); - logger.info("Generating private key to {}", privateFile.getAbsolutePath()); - String privatePEM = toString(keypair.getPrivate()); - FileUtils.write(privateFile, privatePEM); - return privatePEM; + 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)) { @@ -45,4 +86,5 @@ public class PEMGenerator { return writer.toString(); } } + } diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java index d74308cfad..a26843fae1 100644 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java @@ -7,24 +7,24 @@ */ package org.opendaylight.controller.netconf.ssh.osgi; -import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.Optional; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.nio.NioEventLoopGroup; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.net.InetSocketAddress; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; -import org.opendaylight.controller.usermanager.IUserManager; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil.InfixProp; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; -import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,112 +32,56 @@ import org.slf4j.LoggerFactory; * Activator for netconf SSH bundle which creates SSH bridge between netconf client and netconf server. Activator * starts SSH Server in its own thread. This thread is closed when activator calls stop() method. Server opens socket * and listens for client connections. Each client connection creation is handled in separate - * {@link org.opendaylight.controller.netconf.ssh.threads.SocketThread} thread. + * {@link org.opendaylight.controller.netconf.ssh.threads.Handshaker} thread. * This thread creates two additional threads {@link org.opendaylight.controller.netconf.ssh.threads.IOThread} * forwarding data from/to client.IOThread closes servers session and server connection when it gets -1 on input stream. * {@link org.opendaylight.controller.netconf.ssh.threads.IOThread}'s run method waits for -1 on input stream to finish. * All threads are daemons. - **/ -public class NetconfSSHActivator implements BundleActivator{ + */ +public class NetconfSSHActivator implements BundleActivator { + private static final Logger logger = LoggerFactory.getLogger(NetconfSSHActivator.class); private NetconfSSHServer server; - private static final Logger logger = LoggerFactory.getLogger(NetconfSSHActivator.class); - private IUserManager iUserManager; - private BundleContext context = null; - - private ServiceTrackerCustomizer customizer = new ServiceTrackerCustomizer(){ - @Override - public IUserManager addingService(final ServiceReference reference) { - logger.trace("Service {} added, let there be SSH bridge.", reference); - iUserManager = context.getService(reference); - try { - onUserManagerFound(iUserManager); - } catch (final Exception e) { - logger.trace("Can't start SSH server due to {}",e); - } - return iUserManager; - } - @Override - public void modifiedService(final ServiceReference reference, final IUserManager service) { - logger.trace("Replacing modified service {} in netconf SSH.", reference); - server.addUserManagerService(service); - } - @Override - public void removedService(final ServiceReference reference, final IUserManager service) { - logger.trace("Removing service {} from netconf SSH. " + - "SSH won't authenticate users until IUserManager service will be started.", reference); - removeUserManagerService(); - } - }; - @Override - public void start(final BundleContext context) { - this.context = context; - listenForManagerService(); + public void start(final BundleContext bundleContext) throws IOException { + server = startSSHServer(bundleContext); } @Override public void stop(BundleContext context) throws IOException { - if (server != null){ - server.stop(); - logger.trace("Netconf SSH bridge is down ..."); + if (server != null) { + server.close(); } } - private void startSSHServer() throws IOException { - checkNotNull(this.iUserManager, "No user manager service available."); - logger.trace("Starting netconf SSH bridge."); - final InetSocketAddress sshSocketAddress = NetconfConfigUtil.extractSSHNetconfAddress(context, - NetconfConfigUtil.DEFAULT_NETCONF_SSH_ADDRESS); - final InetSocketAddress tcpSocketAddress = NetconfConfigUtil.extractTCPNetconfClientAddress(context, - NetconfConfigUtil.DEFAULT_NETCONF_TCP_ADDRESS); - String path = FilenameUtils.separatorsToSystem(NetconfConfigUtil.getPrivateKeyPath(context)); + private static NetconfSSHServer startSSHServer(BundleContext bundleContext) throws IOException { + Optional maybeSshSocketAddress = NetconfConfigUtil.extractNetconfServerAddress(bundleContext, + InfixProp.ssh); - if (path.isEmpty()) { - throw new IllegalStateException("Missing netconf.ssh.pk.path key in configuration file."); + if (maybeSshSocketAddress.isPresent() == false) { + logger.trace("SSH bridge not configured"); + return null; } + InetSocketAddress sshSocketAddress = maybeSshSocketAddress.get(); + logger.trace("Starting netconf SSH bridge at {}", sshSocketAddress); - final File privateKeyFile = new File(path); - final String privateKeyPEMString; - if (privateKeyFile.exists() == false) { - // generate & save to file - try { - privateKeyPEMString = PEMGenerator.generateTo(privateKeyFile); - } catch (Exception e) { - logger.error("Exception occurred while generating PEM string {}", e); - throw new IllegalStateException("Error generating RSA key from file " + path); - } - } else { - // read from file - try (FileInputStream fis = new FileInputStream(path)) { - privateKeyPEMString = IOUtils.toString(fis); - } catch (final IOException e) { - logger.error("Error reading RSA key from file '{}'", path); - throw new IOException("Error reading RSA key from file " + path, e); - } - } - final AuthProvider authProvider = new AuthProvider(iUserManager, privateKeyPEMString); - this.server = NetconfSSHServer.start(sshSocketAddress.getPort(), tcpSocketAddress, authProvider); + LocalAddress localAddress = NetconfConfigUtil.getNetconfLocalAddress(); + + String path = FilenameUtils.separatorsToSystem(NetconfConfigUtil.getPrivateKeyPath(bundleContext)); + checkState(StringUtils.isNotBlank(path), "Path to ssh private key is blank. Reconfigure %s", NetconfConfigUtil.getPrivateKeyKey()); + String privateKeyPEMString = PEMGenerator.readOrGeneratePK(new File(path)); + + final AuthProvider authProvider = new AuthProvider(privateKeyPEMString, bundleContext); + EventLoopGroup bossGroup = new NioEventLoopGroup(); + NetconfSSHServer server = NetconfSSHServer.start(sshSocketAddress.getPort(), localAddress, authProvider, bossGroup); final Thread serverThread = new Thread(server, "netconf SSH server thread"); serverThread.setDaemon(true); serverThread.start(); logger.trace("Netconf SSH bridge up and running."); + return server; } - private void onUserManagerFound(final IUserManager userManager) throws Exception{ - if (server!=null && server.isUp()){ - server.addUserManagerService(userManager); - } else { - startSSHServer(); - } - } - private void removeUserManagerService(){ - this.server.removeUserManagerService(); - } - private void listenForManagerService(){ - final ServiceTracker listenerTracker = new ServiceTracker<>(context, IUserManager.class,customizer); - listenerTracker.open(); - } + } 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 new file mode 100644 index 0000000000..d999d378d9 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java @@ -0,0 +1,406 @@ +/* + * 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 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; +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.ssh.authentication.AuthProvider; +import org.opendaylight.controller.netconf.util.messages.NetconfHelloMessageAdditionalHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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) 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(authProvider.getPEMAsCharArray(), 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("{} SocketThread is started", session); + try { + // TODO this should be guarded with a timer to prevent resource exhaustion + ganymedConnection.connect(); + } catch (IOException e) { + logger.warn("{} SocketThread error ", session, e); + } + logger.trace("{} SocketThread 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 OutputStream remoteOutputStream; + private final String session; + private ChannelHandlerContext channelHandlerContext; + + public SSHClientHandler(AutoCloseable remoteConnection, OutputStream remoteOutputStream, + String session) { + this.remoteConnection = remoteConnection; + this.remoteOutputStream = 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) { + 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(); + 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(); + + // write additional header + channel.writeAndFlush(Unpooled.copiedBuffer(additionalHeader.getBytes())); + } 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/main/java/org/opendaylight/controller/netconf/ssh/threads/IOThread.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/IOThread.java deleted file mode 100644 index c53a625ad0..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/IOThread.java +++ /dev/null @@ -1,70 +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 java.io.InputStream; -import java.io.OutputStream; - -import javax.annotation.concurrent.ThreadSafe; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.ethz.ssh2.ServerConnection; -import ch.ethz.ssh2.ServerSession; - -@ThreadSafe -public class IOThread extends Thread { - - private static final Logger logger = LoggerFactory.getLogger(IOThread.class); - - private final InputStream inputStream; - private final OutputStream outputStream; - private final ServerSession servSession; - private final ServerConnection servconnection; - private String customHeader; - - - public IOThread (InputStream is, OutputStream os, String id,ServerSession ss, ServerConnection conn){ - this.inputStream = is; - this.outputStream = os; - this.servSession = ss; - this.servconnection = conn; - super.setName(id); - logger.trace("IOThread {} created", super.getName()); - } - - public IOThread (InputStream is, OutputStream os, String id,ServerSession ss, ServerConnection conn,String header){ - this.inputStream = is; - this.outputStream = os; - this.servSession = ss; - this.servconnection = conn; - this.customHeader = header; - super.setName(id); - logger.trace("IOThread {} created", super.getName()); - } - - @Override - public void run() { - logger.trace("thread {} started", super.getName()); - try { - if (this.customHeader!=null && !this.customHeader.equals("")){ - this.outputStream.write(this.customHeader.getBytes()); - logger.trace("adding {} header", this.customHeader); - } - IOUtils.copy(this.inputStream, this.outputStream); - } catch (Exception e) { - logger.error("inputstream -> outputstream copy error ",e); - } - logger.trace("closing server session"); - servSession.close(); - servconnection.close(); - logger.trace("thread {} is closing",super.getName()); - } -} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/SocketThread.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/SocketThread.java deleted file mode 100644 index 04639cb36f..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/SocketThread.java +++ /dev/null @@ -1,208 +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 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 org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.concurrent.ThreadSafe; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; - -@ThreadSafe -public class SocketThread implements Runnable, ServerAuthenticationCallback, ServerConnectionCallback { - private static final Logger logger = LoggerFactory.getLogger(SocketThread.class); - - private final Socket socket; - private final InetSocketAddress clientAddress; - private ServerConnection conn = null; - private final long sessionId; - private String currentUser; - private final String remoteAddressWithPort; - private final AuthProvider authProvider; - - - public static void start(Socket socket, - InetSocketAddress clientAddress, - long sessionId, - AuthProvider authProvider) throws IOException { - Thread netconf_ssh_socket_thread = new Thread(new SocketThread(socket, clientAddress, sessionId, authProvider)); - netconf_ssh_socket_thread.setDaemon(true); - netconf_ssh_socket_thread.start(); - } - - private SocketThread(Socket socket, - InetSocketAddress clientAddress, - long sessionId, - AuthProvider authProvider) throws IOException { - - this.socket = socket; - this.clientAddress = clientAddress; - this.sessionId = sessionId; - this.remoteAddressWithPort = socket.getRemoteSocketAddress().toString().replaceFirst("/", ""); - this.authProvider = authProvider; - - } - - @Override - public void run() { - conn = new ServerConnection(socket); - try { - conn.setPEMHostKey(authProvider.getPEMAsCharArray(), "netconf"); - } catch (Exception e) { - logger.warn("Server authentication setup failed.", e); - } - conn.setAuthenticationCallback(this); - conn.setServerConnectionCallback(this); - try { - conn.connect(); - } catch (IOException e) { - logger.error("SocketThread error ", e); - } - } - - @Override - public ServerSessionCallback acceptSession(final ServerSession session) { - SimpleServerSessionCallback cb = new SimpleServerSessionCallback() { - @Override - public Runnable requestSubsystem(final ServerSession ss, final String subsystem) throws IOException { - return new Runnable() { - @Override - public void run() { - if (subsystem.equals("netconf")) { - IOThread netconf_ssh_input = null; - IOThread netconf_ssh_output = null; - try { - String hostName = clientAddress.getHostName(); - int portNumber = clientAddress.getPort(); - final Socket echoSocket = new Socket(hostName, portNumber); - logger.trace("echo socket created"); - - logger.trace("starting netconf_ssh_input thread"); - netconf_ssh_input = new IOThread(echoSocket.getInputStream(), ss.getStdin(), "input_thread_" + sessionId, ss, conn); - netconf_ssh_input.setDaemon(false); - netconf_ssh_input.start(); - - logger.trace("starting netconf_ssh_output thread"); - final String customHeader = "[" + currentUser + ";" + remoteAddressWithPort + ";ssh;;;;;;]\n"; - netconf_ssh_output = new IOThread(ss.getStdout(), echoSocket.getOutputStream(), "output_thread_" + sessionId, ss, conn, customHeader); - netconf_ssh_output.setDaemon(false); - netconf_ssh_output.start(); - - } catch (Exception t) { - logger.error("SSH bridge could not create echo socket: {}", t.getMessage(), t); - - try { - if (netconf_ssh_input != null) { - netconf_ssh_input.join(); - } - } catch (InterruptedException e1) { - Thread.currentThread().interrupt(); - logger.error("netconf_ssh_input join error ", e1); - } - - try { - if (netconf_ssh_output != null) { - netconf_ssh_output.join(); - } - } catch (InterruptedException e2) { - Thread.currentThread().interrupt(); - logger.error("netconf_ssh_output join error ", e2); - } - } - } else { - String reason = "Only netconf subsystem is supported, requested:" + subsystem; - closeSession(ss, reason); - } - } - }; - } - - public void closeSession(ServerSession ss, String reason) { - logger.trace("Closing session - {}", reason); - try { - ss.getStdin().write(reason.getBytes()); - } catch (IOException e) { - logger.debug("Exception while closing session", 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"); - } - }; - } - }; - - return cb; - } - - @Override - public String initAuthentication(ServerConnection sc) { - logger.trace("Established connection with host {}", remoteAddressWithPort); - return "Established connection with host " + remoteAddressWithPort + "\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) { - - try { - if (authProvider.authenticated(username, password)) { - currentUser = username; - logger.trace("user {}@{} authenticated", currentUser, remoteAddressWithPort); - return AuthenticationResult.SUCCESS; - } - } catch (Exception e) { - logger.warn("Authentication failed due to :" + e.getLocalizedMessage()); - } - return AuthenticationResult.FAILURE; - } - - @Override - public AuthenticationResult authenticateWithPublicKey(ServerConnection sc, String username, String algorithm, - byte[] publickey, byte[] signature) { - return AuthenticationResult.FAILURE; - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/KeyGeneratorTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/KeyGeneratorTest.java deleted file mode 100644 index 298f91ce8d..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/KeyGeneratorTest.java +++ /dev/null @@ -1,65 +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; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; -import org.opendaylight.controller.usermanager.IUserManager; -import org.opendaylight.controller.usermanager.UserConfig; - -import java.io.File; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.InetSocketAddress; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doReturn; - -// This test is intended to be verified using ssh -@Ignore -public class KeyGeneratorTest { - - @Mock - private IUserManager iUserManager; - File tempFile; - - @Before - public void setUp() throws IOException { - MockitoAnnotations.initMocks(this); - doReturn(null).when(iUserManager).addLocalUser(any(UserConfig.class)); - tempFile = File.createTempFile("odltest", ".tmp"); - tempFile.deleteOnExit(); - } - - @After - public void tearDown() { - assertTrue(tempFile.delete()); - } - - @Test - public void test() throws Exception { - String pem = PEMGenerator.generateTo(tempFile); - - AuthProvider authProvider = new AuthProvider(iUserManager, pem); - InetSocketAddress inetSocketAddress = new InetSocketAddress(Inet4Address.getLoopbackAddress().getHostAddress(), 8383); - NetconfSSHServer server = NetconfSSHServer.start(1830, inetSocketAddress, authProvider); - - Thread serverThread = new Thread(server,"netconf SSH server thread"); - serverThread.start(); - serverThread.join(); - } -} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/SSHServerTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/SSHServerTest.java deleted file mode 100644 index 663a0b4a82..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/SSHServerTest.java +++ /dev/null @@ -1,68 +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; - -import ch.ethz.ssh2.Connection; -import junit.framework.Assert; -import org.apache.commons.io.IOUtils; -import org.junit.Test; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.InputStream; -import java.net.InetSocketAddress; - - -public class SSHServerTest { - - private static final String USER = "netconf"; - 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; - - - - - public void startSSHServer() throws Exception{ - logger.info("Creating SSH server"); - StubUserManager um = new StubUserManager(USER,PASSWORD); - String pem; - try(InputStream is = getClass().getResourceAsStream("/RSA.pk")) { - pem = IOUtils.toString(is); - } - AuthProvider ap = new AuthProvider(um, pem); - NetconfSSHServer server = NetconfSSHServer.start(PORT,tcpAddress,ap); - sshServerThread = new Thread(server); - sshServerThread.setDaemon(true); - sshServerThread.start(); - logger.info("SSH server on"); - } - - @Test - public void connect(){ - try { - this.startSSHServer(); - 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); - } - - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClient.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClient.java new file mode 100644 index 0000000000..5d0c71aa62 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClient.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014 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.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +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.channel.nio.NioEventLoopGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Sends one message when a connection is open and echoes back any received + * data to the server. Simply put, the echo client initiates the ping-pong + * traffic between the echo client and server by sending the first message to + * the server. + */ +public class EchoClient implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(EchoClient.class); + + private final ChannelHandler clientHandler; + + + public EchoClient(ChannelHandler clientHandler) { + this.clientHandler = clientHandler; + } + + public void run() { + // Configure the client. + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(LocalChannel.class) + .handler(new ChannelInitializer() { + @Override + public void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(clientHandler); + } + }); + + // Start the client. + LocalAddress localAddress = new LocalAddress("foo"); + ChannelFuture f = b.connect(localAddress).sync(); + + // Wait until the connection is closed. + f.channel().closeFuture().sync(); + } catch (Exception e) { + logger.error("Error in client", e); + throw new RuntimeException("Error in client", e); + } finally { + // Shut down the event loop to terminate all threads. + logger.info("Client is shutting down"); + group.shutdownGracefully(); + } + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClientHandler.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClientHandler.java new file mode 100644 index 0000000000..81182a580e --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoClientHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014 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.netty; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.Charsets; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler implementation for the echo client. It initiates the ping-pong + * traffic between the echo client and server by sending the first message to + * the server. + */ +public class EchoClientHandler extends ChannelInboundHandlerAdapter { + private static final Logger logger = LoggerFactory.getLogger(EchoClientHandler.class); + + private ChannelHandlerContext ctx; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + checkState(this.ctx == null); + logger.info("client active"); + this.ctx = ctx; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf bb = (ByteBuf) msg; + logger.info(">{}", bb.toString(Charsets.UTF_8)); + bb.release(); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + logger.warn("Unexpected exception from downstream.", cause); + checkState(this.ctx.equals(ctx)); + ctx.close(); + this.ctx = null; + } + + public void write(String message) { + ByteBuf byteBuf = Unpooled.copiedBuffer(message.getBytes()); + ctx.writeAndFlush(byteBuf); + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServer.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServer.java new file mode 100644 index 0000000000..ec89d75f29 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServer.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 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.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Echoes back any received data from a client. + */ +public class EchoServer implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(EchoServer.class); + + public void run() { + // Configure the server. + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(LocalServerChannel.class) + .option(ChannelOption.SO_BACKLOG, 100) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(new EchoServerHandler()); + } + }); + + // Start the server. + LocalAddress localAddress = NetconfConfigUtil.getNetconfLocalAddress(); + ChannelFuture f = b.bind(localAddress).sync(); + + // Wait until the server socket is closed. + f.channel().closeFuture().sync(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + // Shut down all event loops to terminate all threads. + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + new Thread(new EchoServer()).start(); + Thread.sleep(1000); + EchoClientHandler clientHandler = new EchoClientHandler(); + EchoClient echoClient = new EchoClient(clientHandler); + new Thread(echoClient).start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + do { + String message = reader.readLine(); + if (message == null || "exit".equalsIgnoreCase(message)) { + break; + } + logger.debug("Got '{}'", message); + clientHandler.write(message); + } while (true); + System.exit(0); + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServerHandler.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServerHandler.java new file mode 100644 index 0000000000..1286ec6875 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/EchoServerHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2014 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.netty; + +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler implementation for the echo server. + */ +@Sharable +public class EchoServerHandler extends ChannelInboundHandlerAdapter { + + private static final Logger logger = LoggerFactory.getLogger(EchoServerHandler.class.getName()); + private String fromLastNewLine = ""; + private final Splitter splitter = Splitter.onPattern("\r?\n"); + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + logger.debug("sleep start"); + Thread.sleep(1000); + logger.debug("sleep done"); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf byteBuf = (ByteBuf) msg; + String message = byteBuf.toString(Charsets.UTF_8); + logger.info("writing back '{}'", message); + ctx.write(msg); + fromLastNewLine += message; + for (String line : splitter.split(fromLastNewLine)) { + if ("quit".equals(line)) { + logger.info("closing server ctx"); + ctx.flush(); + ctx.close(); + break; + } + fromLastNewLine = line; // last line should be preserved + } + + // do not release byteBuf as it is handled back + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + logger.debug("flushing"); + ctx.flush(); + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServer.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServer.java new file mode 100644 index 0000000000..8f2c50278d --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServer.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014 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.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.net.InetSocketAddress; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; + +public class ProxyServer implements Runnable { + private final ProxyHandlerFactory proxyHandlerFactory; + + public ProxyServer(ProxyHandlerFactory proxyHandlerFactory) { + this.proxyHandlerFactory = proxyHandlerFactory; + } + + public void run() { + // Configure the server. + final EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + final LocalAddress localAddress = NetconfConfigUtil.getNetconfLocalAddress(); + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 100) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(proxyHandlerFactory.create(bossGroup, localAddress)); + } + }); + + // Start the server. + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8080); + ChannelFuture f = serverBootstrap.bind(address).sync(); + + // Wait until the server socket is closed. + f.channel().closeFuture().sync(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + // Shut down all event loops to terminate all threads. + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } + public static interface ProxyHandlerFactory { + ChannelHandler create(EventLoopGroup bossGroup, LocalAddress localAddress); + } + + public static void main(String[] args) { + ProxyHandlerFactory proxyHandlerFactory = new ProxyHandlerFactory() { + @Override + public ChannelHandler create(EventLoopGroup bossGroup, LocalAddress localAddress) { + return new ProxyServerHandler(bossGroup, localAddress); + } + }; + start(proxyHandlerFactory); + } + + public static void start(ProxyHandlerFactory proxyHandlerFactory) { + new Thread(new EchoServer()).start(); + new Thread(new ProxyServer(proxyHandlerFactory)).start(); + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServerHandler.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServerHandler.java new file mode 100644 index 0000000000..ecab21256e --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/ProxyServerHandler.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2014 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.netty; + +import com.google.common.base.Charsets; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProxyServerHandler extends ChannelInboundHandlerAdapter { + private static final Logger logger = LoggerFactory.getLogger(ProxyServerHandler.class.getName()); + private final Bootstrap clientBootstrap; + private final LocalAddress localAddress; + + + private Channel clientChannel; + + public ProxyServerHandler(EventLoopGroup bossGroup, LocalAddress localAddress) { + clientBootstrap = new Bootstrap(); + clientBootstrap.group(bossGroup).channel(LocalChannel.class); + this.localAddress = localAddress; + } + + @Override + public void channelActive(ChannelHandlerContext remoteCtx) { + final ProxyClientHandler clientHandler = new ProxyClientHandler(remoteCtx); + clientBootstrap.handler(new ChannelInitializer() { + @Override + public void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(clientHandler); + } + }); + ChannelFuture clientChannelFuture = clientBootstrap.connect(localAddress).awaitUninterruptibly(); + clientChannel = clientChannelFuture.channel(); + clientChannel.writeAndFlush(Unpooled.copiedBuffer("connected\n".getBytes())); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + logger.info("channelInactive - closing client connection"); + clientChannel.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, final Object msg) { + logger.debug("Writing to client {}", msg); + clientChannel.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + logger.debug("flushing"); + clientChannel.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + logger.warn("Unexpected exception from downstream.", cause); + ctx.close(); + } +} + +class ProxyClientHandler extends ChannelInboundHandlerAdapter { + private static final Logger logger = LoggerFactory.getLogger(ProxyClientHandler.class); + + private final ChannelHandlerContext remoteCtx; + + + public ProxyClientHandler(ChannelHandlerContext remoteCtx) { + this.remoteCtx = remoteCtx; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + logger.info("client active"); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf bb = (ByteBuf) msg; + logger.info(">{}", bb.toString(Charsets.UTF_8)); + remoteCtx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + logger.debug("Flushing server ctx"); + remoteCtx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + logger.warn("Unexpected exception from downstream", cause); + ctx.close(); + } + + // called both when local or remote connection dies + @Override + public void channelInactive(ChannelHandlerContext ctx) { + logger.debug("channelInactive() called, closing remote client ctx"); + remoteCtx.close(); + } +} 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 new file mode 100644 index 0000000000..4e32e82e89 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014 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.netty; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import io.netty.channel.nio.NioEventLoopGroup; +import org.junit.Test; +import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; +import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider; +import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SSHTest { + public static final Logger logger = LoggerFactory.getLogger(SSHTest.class); + + @Test + public void test() throws Exception { + new Thread(new EchoServer(), "EchoServer").start(); + AuthProvider authProvider = mock(AuthProvider.class); + doReturn(PEMGenerator.generate().toCharArray()).when(authProvider).getPEMAsCharArray(); + doReturn(true).when(authProvider).authenticated(anyString(), anyString()); + NetconfSSHServer thread = NetconfSSHServer.start(1831, NetconfConfigUtil.getNetconfLocalAddress(), authProvider, new NioEventLoopGroup()); + Thread.sleep(2000); + logger.info("Closing socket"); + thread.close(); + thread.join(); + } +} 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 new file mode 100644 index 0000000000..5e368bc566 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java @@ -0,0 +1,91 @@ +/* + * 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 static org.mockito.Matchers.any; +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 org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opendaylight.controller.netconf.StubUserManager; +import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class SSHServerTest { + + private static final String USER = "netconf"; + 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; + + @Mock + private BundleContext mockedContext; + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(null).when(mockedContext).createFilter(anyString()); + doNothing().when(mockedContext).addServiceListener(any(ServiceListener.class), anyString()); + doReturn(new ServiceReference[0]).when(mockedContext).getServiceReferences(anyString(), anyString()); + + logger.info("Creating SSH server"); + StubUserManager um = new StubUserManager(USER, PASSWORD); + String pem; + try (InputStream is = getClass().getResourceAsStream("/RSA.pk")) { + pem = IOUtils.toString(is); + } + AuthProvider ap = new AuthProvider(pem, mockedContext); + ap.setNullableUserManager(um); + EventLoopGroup bossGroup = new NioEventLoopGroup(); + NetconfSSHServer server = NetconfSSHServer.start(PORT, NetconfConfigUtil.getNetconfLocalAddress(), + ap, bossGroup); + + sshServerThread = new Thread(server); + sshServerThread.setDaemon(true); + sshServerThread.start(); + logger.info("SSH server on " + PORT); + } + + @Test + public void connect() { + 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); + } + + } + +} diff --git a/opendaylight/netconf/netconf-ssh/src/test/resources/logback-test.xml b/opendaylight/netconf/netconf-ssh/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..324c234330 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + %date{"yyyy-MM-dd HH:mm:ss.SSS z"} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/opendaylight/netconf/netconf-tcp/pom.xml b/opendaylight/netconf/netconf-tcp/pom.xml new file mode 100644 index 0000000000..65da6e987e --- /dev/null +++ b/opendaylight/netconf/netconf-tcp/pom.xml @@ -0,0 +1,63 @@ + + + + 4.0.0 + + org.opendaylight.controller + netconf-subsystem + 0.2.5-SNAPSHOT + ../ + + netconf-tcp + bundle + ${project.artifactId} + + + + ${project.groupId} + netconf-api + + + ${project.groupId} + netconf-util + + + commons-io + commons-io + + + org.slf4j + slf4j-api + + + org.opendaylight.yangtools + mockito-configuration + test + + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.7 + + + org.opendaylight.controller.netconf.tcp.osgi.NetconfTCPActivator + com.google.common.base, io.netty.bootstrap, io.netty.channel, io.netty.channel.local, + io.netty.channel.nio, io.netty.channel.socket, io.netty.channel.socket.nio, io.netty.handler.logging, + io.netty.util.concurrent, org.opendaylight.controller.netconf.util.osgi, org.osgi.framework, org.slf4j + + + + + + + diff --git a/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServer.java b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServer.java new file mode 100644 index 0000000000..2e0022cb07 --- /dev/null +++ b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServer.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014 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.tcp.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +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.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.net.InetSocketAddress; + +public class ProxyServer implements AutoCloseable { + private final EventLoopGroup bossGroup = new NioEventLoopGroup(); + private final EventLoopGroup workerGroup = new NioEventLoopGroup(); + private final ChannelFuture channelFuture; + + public ProxyServer(InetSocketAddress address, final LocalAddress localAddress) { + // Configure the server. + final Bootstrap clientBootstrap = new Bootstrap(); + clientBootstrap.group(bossGroup).channel(LocalChannel.class); + + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.DEBUG)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new ProxyServerHandler(clientBootstrap, localAddress)); + } + }); + + // Start the server. + channelFuture = serverBootstrap.bind(address).syncUninterruptibly(); + } + + @Override + public void close() { + channelFuture.channel().close(); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } +} diff --git a/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServerHandler.java b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServerHandler.java new file mode 100644 index 0000000000..fa8892853b --- /dev/null +++ b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/netty/ProxyServerHandler.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2014 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.tcp.netty; + +import static com.google.common.base.Preconditions.checkState; + +import io.netty.bootstrap.Bootstrap; +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.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProxyServerHandler extends ChannelInboundHandlerAdapter { + private static final Logger logger = LoggerFactory.getLogger(ProxyServerHandler.class.getName()); + private final Bootstrap clientBootstrap; + private final LocalAddress localAddress; + + private Channel clientChannel; + + public ProxyServerHandler(Bootstrap clientBootstrap, LocalAddress localAddress) { + this.clientBootstrap = clientBootstrap; + this.localAddress = localAddress; + } + + @Override + public void channelActive(ChannelHandlerContext remoteCtx) { + final ProxyClientHandler clientHandler = new ProxyClientHandler(remoteCtx); + clientBootstrap.handler(new ChannelInitializer() { + @Override + public void initChannel(LocalChannel ch) throws Exception { + ch.pipeline().addLast(clientHandler); + } + }); + ChannelFuture clientChannelFuture = clientBootstrap.connect(localAddress).awaitUninterruptibly(); + clientChannel = clientChannelFuture.channel(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + logger.trace("channelInactive - closing client channel"); + clientChannel.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, final Object msg) { + logger.trace("Writing to client channel"); + clientChannel.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + logger.trace("Flushing client channel"); + clientChannel.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + logger.warn("Unexpected exception from downstream.", cause); + ctx.close(); + } +} + +class ProxyClientHandler extends ChannelInboundHandlerAdapter { + private static final Logger logger = LoggerFactory.getLogger(ProxyClientHandler.class); + + private final ChannelHandlerContext remoteCtx; + private ChannelHandlerContext localCtx; + + public ProxyClientHandler(ChannelHandlerContext remoteCtx) { + this.remoteCtx = remoteCtx; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + checkState(this.localCtx == null); + logger.trace("Client channel active"); + this.localCtx = ctx; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + logger.trace("Forwarding message"); + remoteCtx.write(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + logger.trace("Flushing remote ctx"); + remoteCtx.flush(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // Close the connection when an exception is raised. + logger.warn("Unexpected exception from downstream", cause); + checkState(this.localCtx.equals(ctx)); + ctx.close(); + } + + // called both when local or remote connection dies + @Override + public void channelInactive(ChannelHandlerContext ctx) { + logger.trace("channelInactive() called, closing remote client ctx"); + remoteCtx.close(); + } + +} diff --git a/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/osgi/NetconfTCPActivator.java b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/osgi/NetconfTCPActivator.java new file mode 100644 index 0000000000..bc94e596d7 --- /dev/null +++ b/opendaylight/netconf/netconf-tcp/src/main/java/org/opendaylight/controller/netconf/tcp/osgi/NetconfTCPActivator.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 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.tcp.osgi; + +import com.google.common.base.Optional; +import java.net.InetSocketAddress; +import org.opendaylight.controller.netconf.tcp.netty.ProxyServer; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; +import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil.InfixProp; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Opens TCP port specified in config.ini, creates bridge between this port and local netconf server. + */ +public class NetconfTCPActivator implements BundleActivator { + private static final Logger logger = LoggerFactory.getLogger(NetconfTCPActivator.class); + private ProxyServer proxyServer; + + @Override + public void start(BundleContext context) { + final Optional maybeAddress = NetconfConfigUtil.extractNetconfServerAddress(context, InfixProp.tcp); + if (maybeAddress.isPresent() == false) { + logger.debug("Netconf tcp server is not configured to start"); + return; + } + InetSocketAddress address = maybeAddress.get(); + if (address.getAddress().isAnyLocalAddress()) { + logger.warn("Unprotected netconf TCP address is configured to ANY local address. This is a security risk. " + + "Consider changing {} to 127.0.0.1", NetconfConfigUtil.getNetconfServerAddressKey(InfixProp.tcp)); + } + logger.info("Starting TCP netconf server at {}", address); + proxyServer = new ProxyServer(address, NetconfConfigUtil.getNetconfLocalAddress()); + } + + @Override + public void stop(BundleContext context) { + if (proxyServer != null) { + proxyServer.close(); + } + } +} diff --git a/opendaylight/netconf/netconf-util/pom.xml b/opendaylight/netconf/netconf-util/pom.xml index dcbdcabbba..d9d957c663 100644 --- a/opendaylight/netconf/netconf-util/pom.xml +++ b/opendaylight/netconf/netconf-util/pom.xml @@ -53,13 +53,14 @@ org.apache.felix maven-bundle-plugin + 2.3.7 com.google.common.base, com.google.common.collect, io.netty.channel, io.netty.util.concurrent, javax.annotation, javax.xml.namespace, javax.xml.parsers, javax.xml.transform, javax.xml.transform.dom, javax.xml.transform.stream, javax.xml.validation, javax.xml.xpath, org.opendaylight.controller.netconf.api, org.opendaylight.controller.netconf.mapping.api, - org.osgi.framework, org.slf4j, org.w3c.dom, org.xml.sax + org.osgi.framework, org.slf4j, org.w3c.dom, org.xml.sax,io.netty.channel.local org.opendaylight.controller.netconf.util.* diff --git a/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/osgi/NetconfConfigUtil.java b/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/osgi/NetconfConfigUtil.java index 0993b8ad0c..333fea3493 100644 --- a/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/osgi/NetconfConfigUtil.java +++ b/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/osgi/NetconfConfigUtil.java @@ -9,36 +9,35 @@ package org.opendaylight.controller.netconf.util.osgi; import com.google.common.base.Optional; +import io.netty.channel.local.LocalAddress; +import java.net.InetSocketAddress; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.InetSocketAddress; - public final class NetconfConfigUtil { private static final Logger logger = LoggerFactory.getLogger(NetconfConfigUtil.class); - public static final InetSocketAddress DEFAULT_NETCONF_TCP_ADDRESS - = new InetSocketAddress("127.0.0.1", 8383); - public static final InetSocketAddress DEFAULT_NETCONF_SSH_ADDRESS - = new InetSocketAddress("0.0.0.0", 1830); - private static final String PREFIX_PROP = "netconf."; private NetconfConfigUtil() { } - private enum InfixProp { + public enum InfixProp { tcp, ssh } private static final String PORT_SUFFIX_PROP = ".port"; private static final String ADDRESS_SUFFIX_PROP = ".address"; - private static final String CLIENT_PROP = ".client"; private static final String PRIVATE_KEY_PATH_PROP = ".pk.path"; private static final String CONNECTION_TIMEOUT_MILLIS_PROP = "connectionTimeoutMillis"; private static final long DEFAULT_TIMEOUT_MILLIS = 5000; + private static final LocalAddress netconfLocalAddress = new LocalAddress("netconf"); + + public static LocalAddress getNetconfLocalAddress() { + return netconfLocalAddress; + } public static long extractTimeoutMillis(final BundleContext bundleContext) { final String key = PREFIX_PROP + CONNECTION_TIMEOUT_MILLIS_PROP; @@ -54,22 +53,6 @@ public final class NetconfConfigUtil { } } - public static InetSocketAddress extractTCPNetconfServerAddress(final BundleContext context, final InetSocketAddress defaultAddress) { - final Optional extracted = extractNetconfServerAddress(context, InfixProp.tcp); - final InetSocketAddress netconfTcpAddress = getNetconfAddress(defaultAddress, extracted, InfixProp.tcp); - logger.debug("Using {} as netconf tcp address", netconfTcpAddress); - if (netconfTcpAddress.getAddress().isAnyLocalAddress()) { - logger.warn("Unprotected netconf TCP address is configured to ANY local address. This is a security risk. " + - "Consider changing {} to 127.0.0.1", PREFIX_PROP + InfixProp.tcp + ADDRESS_SUFFIX_PROP); - } - return netconfTcpAddress; - } - - public static InetSocketAddress extractTCPNetconfClientAddress(final BundleContext context, final InetSocketAddress defaultAddress) { - final Optional extracted = extractNetconfClientAddress(context, InfixProp.tcp); - return getNetconfAddress(defaultAddress, extracted, InfixProp.tcp); - } - /** * Get extracted address or default. * @@ -93,15 +76,12 @@ public final class NetconfConfigUtil { return inetSocketAddress; } - public static InetSocketAddress extractSSHNetconfAddress(final BundleContext context, final InetSocketAddress defaultAddress) { - Optional extractedAddress = extractNetconfServerAddress(context, InfixProp.ssh); - InetSocketAddress netconfSSHAddress = getNetconfAddress(defaultAddress, extractedAddress, InfixProp.ssh); - logger.debug("Using {} as netconf SSH address", netconfSSHAddress); - return netconfSSHAddress; + public static String getPrivateKeyPath(final BundleContext context) { + return getPropertyValue(context, getPrivateKeyKey()); } - public static String getPrivateKeyPath(final BundleContext context) { - return getPropertyValue(context, PREFIX_PROP + InfixProp.ssh + PRIVATE_KEY_PATH_PROP); + public static String getPrivateKeyKey() { + return PREFIX_PROP + InfixProp.ssh + PRIVATE_KEY_PATH_PROP; } private static String getPropertyValue(final BundleContext context, final String propertyName) { @@ -112,16 +92,20 @@ public final class NetconfConfigUtil { return propertyValue; } + public static String getNetconfServerAddressKey(InfixProp infixProp) { + return PREFIX_PROP + infixProp + ADDRESS_SUFFIX_PROP; + } + /** * @param context from which properties are being read. * @param infixProp either tcp or ssh * @return value if address and port are present and valid, Optional.absent otherwise. * @throws IllegalStateException if address or port are invalid, or configuration is missing */ - private static Optional extractNetconfServerAddress(final BundleContext context, + public static Optional extractNetconfServerAddress(final BundleContext context, final InfixProp infixProp) { - final Optional address = getProperty(context, PREFIX_PROP + infixProp + ADDRESS_SUFFIX_PROP); + final Optional address = getProperty(context, getNetconfServerAddressKey(infixProp)); final Optional port = getProperty(context, PREFIX_PROP + infixProp + PORT_SUFFIX_PROP); if (address.isPresent() && port.isPresent()) { @@ -140,24 +124,6 @@ public final class NetconfConfigUtil { return new InetSocketAddress(address.get(), portNumber); } - private static Optional extractNetconfClientAddress(final BundleContext context, - final InfixProp infixProp) { - final Optional address = getProperty(context, - PREFIX_PROP + infixProp + CLIENT_PROP + ADDRESS_SUFFIX_PROP); - final Optional port = getProperty(context, - PREFIX_PROP + infixProp + CLIENT_PROP + PORT_SUFFIX_PROP); - - if (address.isPresent() && port.isPresent()) { - try { - return Optional.of(parseAddress(address, port)); - } catch (final RuntimeException e) { - logger.warn("Unable to parse client {} netconf address from {}:{}, fallback to server address", - infixProp, address, port, e); - } - } - return extractNetconfServerAddress(context, infixProp); - } - private static Optional getProperty(final BundleContext context, final String propKey) { String value = context.getProperty(propKey); if (value != null && value.isEmpty()) { diff --git a/opendaylight/netconf/pom.xml b/opendaylight/netconf/pom.xml index 4f87fd8626..e1a9cada19 100644 --- a/opendaylight/netconf/pom.xml +++ b/opendaylight/netconf/pom.xml @@ -27,6 +27,7 @@ netconf-mapping-api netconf-client netconf-ssh + netconf-tcp netconf-monitoring ietf-netconf-monitoring ietf-netconf-monitoring-extension -- 2.36.6