<version>${netconf.version}</version>
<type>test-jar</type>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>netconf-tcp</artifactId>
+ <version>${netconf.version}</version>
+ </dependency>
<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>netconf-util</artifactId>
*/
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;
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<S extends ProtocolSession<?>, L extends SessionListener<?, ?, ?>> implements Closeable {
- protected interface PipelineInitializer<S extends ProtocolSession<?>> {
+
+ protected interface ChannelPipelineInitializer<CH extends Channel, S extends ProtocolSession<?>> {
/**
* Initializes channel by specifying the handlers in its pipeline. Handlers are protocol specific, therefore this
* method needs to be implemented in protocol specific Dispatchers.
* @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<S> promise);
+ void initializeChannel(CH channel, Promise<S> promise);
+ }
+
+ protected interface PipelineInitializer<S extends ProtocolSession<?>> extends ChannelPipelineInitializer<SocketChannel, S> {
+
}
* @return ChannelFuture representing the binding process
*/
protected ChannelFuture createServer(final InetSocketAddress address, final PipelineInitializer<S> 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 <CH extends Channel> ChannelFuture createServer(SocketAddress address, Class<? extends ServerChannel> channelClass,
+ final ChannelPipelineInitializer<CH, S> initializer) {
final ServerBootstrap b = new ServerBootstrap();
- b.childHandler(new ChannelInitializer<SocketChannel>() {
+ b.childHandler(new ChannelInitializer<CH>() {
@Override
- protected void initChannel(final SocketChannel ch) {
+ protected void initChannel(final CH ch) {
initializer.initializeChannel(ch, new DefaultPromise<S>(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);
}
<groupId>org.opendaylight.controller</groupId>
<artifactId>netconf-ssh</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>netconf-tcp</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>netconf-util</artifactId>
# 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
*/
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;
}
@Override
- public void initialize(final SocketChannel ch, final Promise<NetconfClientSession> promise) {
+ public void initialize(final Channel ch, final Promise<NetconfClientSession> promise) {
try {
final Invoker invoker = Invoker.subsystem("netconf");
ch.pipeline().addFirst(new SshHandler(authenticationHandler, invoker));
}
@Override
- protected void initializeSessionNegotiator(final SocketChannel ch,
+ protected void initializeSessionNegotiator(final Channel ch,
final Promise<NetconfClientSession> promise) {
ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR,
negotiatorFactory.getSessionNegotiator(new SessionListenerFactory<NetconfClientSessionListener>() {
*/
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;
}
@Override
- public void initialize(final SocketChannel ch, final Promise<NetconfClientSession> promise) {
- super.initialize(ch, promise);
- }
-
- @Override
- protected void initializeSessionNegotiator(final SocketChannel ch, final Promise<NetconfClientSession> promise) {
+ protected void initializeSessionNegotiator(final Channel ch, final Promise<NetconfClientSession> promise) {
ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR,
negotiatorFactory.getSessionNegotiator(new SessionListenerFactory<NetconfClientSessionListener>() {
@Override
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;
/**
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<? extends AuthenticationHandler> 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();
+ }
}
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
+ <version>2.3.7</version>
<configuration>
<instructions>
<Bundle-Activator>org.opendaylight.controller.netconf.impl.osgi.NetconfImplActivator</Bundle-Activator>
io.netty.buffer,
io.netty.handler.codec,
io.netty.channel.nio,
+ io.netty.channel.local,
javax.annotation,
javax.management,
javax.net.ssl,
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;
this.initializer = serverChannelInitializer;
}
+ @VisibleForTesting
public ChannelFuture createServer(InetSocketAddress address) {
return super.createServer(address, new PipelineInitializer<NetconfServerSession>() {
});
}
+ public ChannelFuture createLocalServer(LocalAddress address) {
+ return super.createServer(address, LocalServerChannel.class, new ChannelPipelineInitializer<LocalChannel, NetconfServerSession>() {
+ @Override
+ public void initializeChannel(final LocalChannel ch, final Promise<NetconfServerSession> promise) {
+ initializer.initialize(ch, promise);
+ }
+ });
+ }
+
public static class ServerChannelInitializer extends AbstractChannelInitializer<NetconfServerSession> {
public static final String DESERIALIZER_EX_HANDLER_KEY = "deserializerExHandler";
}
@Override
- protected void initializeMessageDecoder(SocketChannel ch) {
+ protected void initializeMessageDecoder(Channel ch) {
super.initializeMessageDecoder(ch);
ch.pipeline().addLast(DESERIALIZER_EX_HANDLER_KEY, new DeserializerExceptionHandler());
}
@Override
- protected void initializeSessionNegotiator(SocketChannel ch, Promise<NetconfServerSession> promise) {
+ protected void initializeSessionNegotiator(Channel ch, Promise<NetconfServerSession> promise) {
ch.pipeline().addAfter(DESERIALIZER_EX_HANDLER_KEY, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR,
negotiatorFactory.getSessionNegotiator(null, ch, promise));
}
}
-
}
*/
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;
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);
private ServiceRegistration<NetconfMonitoringService> 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);
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<String, ?> dic = new Hashtable<>();
+ private NetconfMonitoringServiceImpl startMonitoringService(BundleContext context, NetconfOperationServiceFactoryListenerImpl factoriesListener) {
+ NetconfMonitoringServiceImpl netconfMonitoringServiceImpl = new NetconfMonitoringServiceImpl(factoriesListener);
+ Dictionary<String, ?> dic = new Hashtable<>();
regMonitoring = context.registerService(NetconfMonitoringService.class, netconfMonitoringServiceImpl, dic);
return netconfMonitoringServiceImpl;
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;
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;
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) {
}
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 {
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;
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;
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;
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;
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");
}
yangDependencies.add(resourceAsStream);
}
}
- assertEquals("Some yang files were not found", emptyList(), failedToFind);
+ assertEquals("Some yang files were not found", Collections.<String>emptyList(), failedToFind);
return yangDependencies;
}
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());
}
}
}
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;
public static final String NETCONF_MESSAGE_FRAME_ENCODER = "frameEncoder";
public static final String NETCONF_SESSION_NEGOTIATOR = "negotiator";
- public void initialize(SocketChannel ch, Promise<S> promise) {
+ public void initialize(Channel ch, Promise<S> promise) {
ch.pipeline().addLast(NETCONF_MESSAGE_AGGREGATOR, new NetconfEOMAggregator());
initializeMessageDecoder(ch);
ch.pipeline().addLast(NETCONF_MESSAGE_FRAME_ENCODER, FramingMechanismHandlerFactory.createHandler(FramingMechanism.EOM));
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());
* 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<S> promise);
+ protected abstract void initializeSessionNegotiator(Channel ch, Promise<S> promise);
}
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
+ <version>2.3.7</version>
<configuration>
<instructions>
<Bundle-Activator>org.opendaylight.controller.netconf.ssh.osgi.NetconfSSHActivator</Bundle-Activator>
<Import-Package>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</Import-Package>
+ 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</Import-Package>
</instructions>
</configuration>
</plugin>
*/
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");
}
}
*/
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<IUserManager, IUserManager> customizer = new ServiceTrackerCustomizer<IUserManager, IUserManager>() {
+ @Override
+ public IUserManager addingService(final ServiceReference<IUserManager> reference) {
+ logger.trace("Service {} added", reference);
+ nullableUserManager = bundleContext.getService(reference);
+ return nullableUserManager;
+ }
+
+ @Override
+ public void modifiedService(final ServiceReference<IUserManager> reference, final IUserManager service) {
+ logger.trace("Replacing modified service {} in netconf SSH.", reference);
+ nullableUserManager = service;
+ }
+
+ @Override
+ public void removedService(final ServiceReference<IUserManager> 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<IUserManager, IUserManager> 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;
}
}
+++ /dev/null
-
-/*
- * 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);
-}
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;
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)) {
return writer.toString();
}
}
+
}
*/
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;
* 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<IUserManager, IUserManager> customizer = new ServiceTrackerCustomizer<IUserManager, IUserManager>(){
- @Override
- public IUserManager addingService(final ServiceReference<IUserManager> 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<IUserManager> reference, final IUserManager service) {
- logger.trace("Replacing modified service {} in netconf SSH.", reference);
- server.addUserManagerService(service);
- }
- @Override
- public void removedService(final ServiceReference<IUserManager> 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<InetSocketAddress> 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<IUserManager, IUserManager> listenerTracker = new ServiceTracker<>(context, IUserManager.class,customizer);
- listenerTracker.open();
- }
+
}
--- /dev/null
+/*
+ * 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<String> 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<String> 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<LocalChannel>() {
+ @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<String> {
+ 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;
+ }
+}
+++ /dev/null
-/*
- * 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());
- }
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
+++ /dev/null
-/*
- * 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();
- }
-}
+++ /dev/null
-/*
- * 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);
- }
-
- }
-
-}
--- /dev/null
+/*
+ * 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<LocalChannel>() {
+ @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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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<LocalChannel>() {
+ @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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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<SocketChannel>() {
+ @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();
+ }
+}
--- /dev/null
+/*
+ * 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<LocalChannel>() {
+ @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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+ }
+
+}
--- /dev/null
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%date{"yyyy-MM-dd HH:mm:ss.SSS z"} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="error">
+ <appender-ref ref="STDOUT" />
+ </root>
+ <logger name="org.opendaylight.controller.netconf" level="TRACE"/>
+</configuration>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>netconf-subsystem</artifactId>
+ <version>0.2.5-SNAPSHOT</version>
+ <relativePath>../</relativePath>
+ </parent>
+ <artifactId>netconf-tcp</artifactId>
+ <packaging>bundle</packaging>
+ <name>${project.artifactId}</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-util</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>mockito-configuration</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <version>2.3.7</version>
+ <configuration>
+ <instructions>
+ <Bundle-Activator>org.opendaylight.controller.netconf.tcp.osgi.NetconfTCPActivator</Bundle-Activator>
+ <Import-Package>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</Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
--- /dev/null
+/*
+ * 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<SocketChannel>() {
+ @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();
+ }
+}
--- /dev/null
+/*
+ * 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<LocalChannel>() {
+ @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();
+ }
+
+}
--- /dev/null
+/*
+ * 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<InetSocketAddress> 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();
+ }
+ }
+}
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
+ <version>2.3.7</version>
<configuration>
<instructions>
<Import-Package>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</Import-Package>
+ org.osgi.framework, org.slf4j, org.w3c.dom, org.xml.sax,io.netty.channel.local</Import-Package>
<Export-Package>org.opendaylight.controller.netconf.util.*</Export-Package>
</instructions>
</configuration>
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;
}
}
- public static InetSocketAddress extractTCPNetconfServerAddress(final BundleContext context, final InetSocketAddress defaultAddress) {
- final Optional<InetSocketAddress> 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<InetSocketAddress> extracted = extractNetconfClientAddress(context, InfixProp.tcp);
- return getNetconfAddress(defaultAddress, extracted, InfixProp.tcp);
- }
-
/**
* Get extracted address or default.
*
return inetSocketAddress;
}
- public static InetSocketAddress extractSSHNetconfAddress(final BundleContext context, final InetSocketAddress defaultAddress) {
- Optional<InetSocketAddress> 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) {
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<InetSocketAddress> extractNetconfServerAddress(final BundleContext context,
+ public static Optional<InetSocketAddress> extractNetconfServerAddress(final BundleContext context,
final InfixProp infixProp) {
- final Optional<String> address = getProperty(context, PREFIX_PROP + infixProp + ADDRESS_SUFFIX_PROP);
+ final Optional<String> address = getProperty(context, getNetconfServerAddressKey(infixProp));
final Optional<String> port = getProperty(context, PREFIX_PROP + infixProp + PORT_SUFFIX_PROP);
if (address.isPresent() && port.isPresent()) {
return new InetSocketAddress(address.get(), portNumber);
}
- private static Optional<InetSocketAddress> extractNetconfClientAddress(final BundleContext context,
- final InfixProp infixProp) {
- final Optional<String> address = getProperty(context,
- PREFIX_PROP + infixProp + CLIENT_PROP + ADDRESS_SUFFIX_PROP);
- final Optional<String> 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<String> getProperty(final BundleContext context, final String propKey) {
String value = context.getProperty(propKey);
if (value != null && value.isEmpty()) {
<module>netconf-mapping-api</module>
<module>netconf-client</module>
<module>netconf-ssh</module>
+ <module>netconf-tcp</module>
<module>netconf-monitoring</module>
<module>ietf-netconf-monitoring</module>
<module>ietf-netconf-monitoring-extension</module>