From: Tony Tkacik Date: Fri, 6 Jun 2014 09:48:52 +0000 (+0000) Subject: Merge "BUG-625: migrate InventoryAndReadAdapter" X-Git-Tag: release/helium~700 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=1060b7d9ccea26ae56e25e499abf66732a53f472;hp=97f6ae6ef4fa1f2f57402d4daf9605be1788968d Merge "BUG-625: migrate InventoryAndReadAdapter" --- diff --git a/opendaylight/commons/opendaylight/pom.xml b/opendaylight/commons/opendaylight/pom.xml index 80be29570d..077b452f0a 100644 --- a/opendaylight/commons/opendaylight/pom.xml +++ b/opendaylight/commons/opendaylight/pom.xml @@ -950,6 +950,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/config/pom.xml b/opendaylight/config/pom.xml index 613940144b..5f34a9aa82 100644 --- a/opendaylight/config/pom.xml +++ b/opendaylight/config/pom.xml @@ -152,6 +152,35 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.12 + + false + false + checkstyle-logging.xml + true + true + ${project.basedir} + **\/*.java,**\/*.xml,**\/*.ini,**\/*.sh,**\/*.bat,**\/*.yang + **\/target\/,**\/bin\/,**\/target-ide\/,**\/${jmxGeneratorPath}\/,**\/${salGeneratorPath}\/ + + + + org.opendaylight.yangtools + checkstyle-logging + ${yangtools.version} + + + + + + check + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/opendaylight/config/threadpool-config-impl/src/test/java/org/opendaylight/controller/config/threadpool/fixed/FixedThreadPoolConfigBeanTest.java b/opendaylight/config/threadpool-config-impl/src/test/java/org/opendaylight/controller/config/threadpool/fixed/FixedThreadPoolConfigBeanTest.java index af7f01a632..62b295be8d 100644 --- a/opendaylight/config/threadpool-config-impl/src/test/java/org/opendaylight/controller/config/threadpool/fixed/FixedThreadPoolConfigBeanTest.java +++ b/opendaylight/config/threadpool-config-impl/src/test/java/org/opendaylight/controller/config/threadpool/fixed/FixedThreadPoolConfigBeanTest.java @@ -7,12 +7,16 @@ */ package org.opendaylight.controller.config.threadpool.fixed; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.matchers.JUnitMatchers.containsString; import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.List; import javax.management.InstanceAlreadyExistsException; import javax.management.InstanceNotFoundException; import javax.management.ObjectName; @@ -28,8 +32,11 @@ import org.opendaylight.controller.config.yang.threadpool.impl.NamingThreadFacto import org.opendaylight.controller.config.yang.threadpool.impl.NamingThreadFactoryModuleMXBean; import org.opendaylight.controller.config.yang.threadpool.impl.fixed.FixedThreadPoolModuleFactory; import org.opendaylight.controller.config.yang.threadpool.impl.fixed.FixedThreadPoolModuleMXBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { + private static final Logger logger = LoggerFactory.getLogger(FixedThreadPoolConfigBeanTest.class); private FixedThreadPoolModuleFactory factory; private final String nameInstance = "fixedInstance"; @@ -37,7 +44,7 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { @Before public void setUp() { factory = new FixedThreadPoolModuleFactory(); - super.initConfigTransactionManagerImpl(new HardcodedModuleFactoriesResolver(mockedContext,factory, + super.initConfigTransactionManagerImpl(new HardcodedModuleFactoriesResolver(mockedContext, factory, new NamingThreadFactoryModuleFactory())); } @@ -45,7 +52,7 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { public void testCreateBean() throws InstanceAlreadyExistsException, ValidationException, ConflictingVersionException { ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction(); - createFixed(transaction, nameInstance, 2); + createFixed(transaction, nameInstance, 2, nameInstance); transaction.validateConfig(); CommitStatus status = transaction.commit(); @@ -58,7 +65,7 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { public void testReusingOldInstance() throws InstanceAlreadyExistsException, ConflictingVersionException, ValidationException { ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction(); - createFixed(transaction, nameInstance, 4); + createFixed(transaction, nameInstance, 4, nameInstance); transaction.validateConfig(); transaction.commit(); @@ -76,12 +83,12 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { public void testNegative() throws ConflictingVersionException, ValidationException, InstanceAlreadyExistsException { ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction(); - createFixed(transaction, nameInstance, 5); + createFixed(transaction, nameInstance, 5, nameInstance); transaction.commit(); transaction = configRegistryClient.createTransaction(); try { - createFixed(transaction, nameInstance, 0); + createFixed(transaction, nameInstance, 0, nameInstance); fail(); } catch (InstanceAlreadyExistsException e) { assertThat( @@ -90,18 +97,35 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { } } + private int countThreadsByPrefix(String prefix) { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + int result = 0; + List names = new ArrayList<>(); + for (ThreadInfo threadInfo : threadMXBean.dumpAllThreads(false, false)) { + names.add(threadInfo.getThreadName()); + if (threadInfo.getThreadName().startsWith(prefix)) { + result++; + } + } + logger.info("Current threads {}", names); + return result; + } + @Test public void testDestroy() throws InstanceAlreadyExistsException, ValidationException, ConflictingVersionException, - InstanceNotFoundException { + InstanceNotFoundException, InterruptedException { + + String prefix = org.apache.commons.lang3.RandomStringUtils.randomAlphabetic(10); int numberOfThreads = 100; - int threadCount1 = ManagementFactory.getThreadMXBean().getThreadCount(); - assertTrue("Expected less than " + numberOfThreads + " threads, got " + threadCount1, threadCount1 < numberOfThreads); + int threadCount1 = countThreadsByPrefix(prefix); + assertEquals(0, threadCount1); ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction(); - createFixed(transaction, nameInstance, numberOfThreads); + + createFixed(transaction, nameInstance, numberOfThreads, prefix); transaction.commit(); - int threadCount2 = ManagementFactory.getThreadMXBean().getThreadCount(); - assertTrue("Expected more than " + numberOfThreads + " threads, got " + threadCount2, threadCount2 > numberOfThreads); + int threadCount2 = countThreadsByPrefix(prefix); + assertEquals(numberOfThreads, threadCount2); transaction = configRegistryClient.createTransaction(); transaction.destroyModule(factory.getImplementationName(), nameInstance); @@ -109,14 +133,20 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { assertBeanCount(0, factory.getImplementationName()); assertStatus(status, 0, 0, 1); - int threadCount3 = ManagementFactory.getThreadMXBean().getThreadCount(); - assertTrue("Expected less than " + numberOfThreads + " threads, got " + threadCount3, threadCount3 < numberOfThreads); + + for (int i = 0; i < 60; i++) { + if (countThreadsByPrefix(prefix) == 0) { + return; + } + Thread.sleep(1000); + } + assertEquals(0, countThreadsByPrefix(prefix)); } @Test public void testValidationException() throws InstanceAlreadyExistsException { ConfigTransactionJMXClient transaction = configRegistryClient.createTransaction(); - createFixed(transaction, nameInstance, -1); + createFixed(transaction, nameInstance, -1, nameInstance); try { transaction.validateConfig(); fail(); @@ -125,7 +155,7 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { } } - private ObjectName createFixed(ConfigTransactionJMXClient transaction, String name, int numberOfThreads) + private ObjectName createFixed(ConfigTransactionJMXClient transaction, String name, int numberOfThreads, String prefix) throws InstanceAlreadyExistsException { ObjectName nameCreated = transaction.createModule(factory.getImplementationName(), name); FixedThreadPoolModuleMXBean mxBean = transaction.newMXBeanProxy(nameCreated, FixedThreadPoolModuleMXBean.class); @@ -134,7 +164,7 @@ public class FixedThreadPoolConfigBeanTest extends AbstractConfigTest { ObjectName threadFactoryON = transaction.createModule(NamingThreadFactoryModuleFactory.NAME, "naming"); NamingThreadFactoryModuleMXBean namingThreadFactoryModuleMXBean = transaction.newMXBeanProxy(threadFactoryON, NamingThreadFactoryModuleMXBean.class); - namingThreadFactoryModuleMXBean.setNamePrefix("prefix"); + namingThreadFactoryModuleMXBean.setNamePrefix(prefix); mxBean.setThreadFactory(threadFactoryON); 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/md-sal/pom.xml b/opendaylight/md-sal/pom.xml index 94bd0731aa..9222734360 100644 --- a/opendaylight/md-sal/pom.xml +++ b/opendaylight/md-sal/pom.xml @@ -143,12 +143,32 @@ + org.apache.maven.plugins maven-checkstyle-plugin - ${checkstyle.version} + 2.12 + + false + false + checkstyle-logging.xml + true + true + ${project.basedir} + **\/*.java,**\/*.xml,**\/*.ini,**\/*.sh,**\/*.bat,**\/*.yang + **\/target\/,**\/bin\/,**\/target-ide\/,**\/${jmxGeneratorPath}\/,**\/${salGeneratorPath}\/ + + + + org.opendaylight.yangtools + checkstyle-logging + ${yangtools.version} + + - none + + check + 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..d26fcf987e 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 @@ -99,6 +100,35 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.12 + + false + false + checkstyle-logging.xml + true + true + ${project.basedir} + **\/*.java,**\/*.xml,**\/*.ini,**\/*.sh,**\/*.bat,**\/*.yang + **\/target\/,**\/bin\/,**\/target-ide\/,**\/${jmxGeneratorPath}\/,**\/${salGeneratorPath}\/ + + + + org.opendaylight.yangtools + checkstyle-logging + ${yangtools.version} + + + + + + check + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/opendaylight/sal/api/src/main/java/org/opendaylight/controller/sal/packet/TCP.java b/opendaylight/sal/api/src/main/java/org/opendaylight/controller/sal/packet/TCP.java index 4b2badd92d..8253ac46d3 100644 --- a/opendaylight/sal/api/src/main/java/org/opendaylight/controller/sal/packet/TCP.java +++ b/opendaylight/sal/api/src/main/java/org/opendaylight/controller/sal/packet/TCP.java @@ -230,4 +230,12 @@ public class TCP extends Packet { return (BitBufferHelper.getShort(fieldValues.get(DESTPORT))); } + /** + * Get the stored checksum value of the TCP header + * @return short - the checksum + */ + public short getChecksum() { + return (BitBufferHelper.getShort(fieldValues.get(CHECKSUM))); + } + } diff --git a/opendaylight/sal/api/src/test/java/org/opendaylight/controller/sal/packet/TCPTest.java b/opendaylight/sal/api/src/test/java/org/opendaylight/controller/sal/packet/TCPTest.java index 48679c33f2..3e18aedfdb 100644 --- a/opendaylight/sal/api/src/test/java/org/opendaylight/controller/sal/packet/TCPTest.java +++ b/opendaylight/sal/api/src/test/java/org/opendaylight/controller/sal/packet/TCPTest.java @@ -104,4 +104,13 @@ public class TCPTest { Assert.assertTrue(urgentPointer[1] == 10); } + + @Test + public void testGetChecksum() { + TCP tcp = new TCP(); + byte[] udpChecksum = { 0, -56 }; + tcp.hdrFieldsMap.put("Checksum", udpChecksum); + short checksum = tcp.getChecksum(); + Assert.assertTrue(checksum == 200); + } }