From: Moiz Raja Date: Mon, 27 Oct 2014 17:14:58 +0000 (+0000) Subject: Merge "Bug 2055: Handle shard not initialized resiliently" X-Git-Tag: release/lithium~968 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=1bc01a15b1e7811ee59249eab7e815408518e354;hp=c8e6e650e2dd0647dbfed665244445cf64c1e262;p=controller.git Merge "Bug 2055: Handle shard not initialized resiliently" --- diff --git a/features/base/pom.xml b/features/base/pom.xml index b7ab3ca757..cd84eeaf33 100644 --- a/features/base/pom.xml +++ b/features/base/pom.xml @@ -31,10 +31,6 @@ org.opendaylight.controller karaf-tomcat-security - - org.opendaylight.controller.thirdparty - ganymed - com.fasterxml.jackson.core jackson-annotations diff --git a/features/base/src/main/resources/features.xml b/features/base/src/main/resources/features.xml index d7d8e0ddac..d6802acd0e 100644 --- a/features/base/src/main/resources/features.xml +++ b/features/base/src/main/resources/features.xml @@ -36,7 +36,6 @@ wrap:mvn:io.netty/netty-common/${netty.version} wrap:mvn:io.netty/netty-handler/${netty.version} wrap:mvn:io.netty/netty-codec-http/${netty.version} - mvn:org.opendaylight.controller.thirdparty/ganymed/1.2.0-SNAPSHOT odl-base-gemini-web diff --git a/features/netconf-connector/pom.xml b/features/netconf-connector/pom.xml index 03d6fed605..b44fa11657 100644 --- a/features/netconf-connector/pom.xml +++ b/features/netconf-connector/pom.xml @@ -162,12 +162,13 @@ Optional TODO: Remove TODO comments. --> - - org.opendaylight.yangtools - features-test - ${yangtools.version} - test - + + + + + + + org.opendaylight.controller diff --git a/features/netconf/pom.xml b/features/netconf/pom.xml index a944bb4dec..028c16b02f 100644 --- a/features/netconf/pom.xml +++ b/features/netconf/pom.xml @@ -74,10 +74,6 @@ org.opendaylight.controller netconf-netty-util - - org.opendaylight.controller.thirdparty - ganymed - org.apache.sshd sshd-core diff --git a/features/netconf/src/main/resources/features.xml b/features/netconf/src/main/resources/features.xml index 444f20865b..fb668ae15a 100644 --- a/features/netconf/src/main/resources/features.xml +++ b/features/netconf/src/main/resources/features.xml @@ -57,7 +57,6 @@ odl-netconf-mapping-api odl-netconf-util mvn:org.opendaylight.controller/netconf-netty-util/${project.version} - mvn:org.opendaylight.controller.thirdparty/ganymed/${ganymed.version} mvn:org.apache.sshd/sshd-core/${sshd-core.version} mvn:org.openexi/nagasena/${exi.nagasena.version} mvn:io.netty/netty-codec/${netty.version} diff --git a/opendaylight/commons/opendaylight/pom.xml b/opendaylight/commons/opendaylight/pom.xml index ffb9ef746d..198d17a79a 100644 --- a/opendaylight/commons/opendaylight/pom.xml +++ b/opendaylight/commons/opendaylight/pom.xml @@ -106,7 +106,6 @@ 0.5.0-SNAPSHOT 0.5.0-SNAPSHOT 0.7.0-SNAPSHOT - 1.2.0-SNAPSHOT 0.6.0-SNAPSHOT 0.6.0-SNAPSHOT 0.5.0-SNAPSHOT @@ -180,7 +179,7 @@ java target/code-coverage/jacoco.exec target/code-coverage/jacoco-it.exec - org.openflow.openflowj,net.sf.jung2,org.opendaylight.controller.protobuff.messages,ch.ethz.ssh2 + org.openflow.openflowj,net.sf.jung2,org.opendaylight.controller.protobuff.messages Sonar way with Findbugs 1.0.0 1.2.1 @@ -218,21 +217,14 @@ + - ${project.groupId} - ietf-netconf-monitoring - ${netconf.version} - - - ${project.groupId} - ietf-netconf-monitoring-extension - ${netconf.version} - - - ${project.groupId} - netconf-netty-util + org.opendaylight.controller + netconf-artifacts ${netconf.version} + pom + import org.apache.sshd @@ -863,11 +855,6 @@ config-manager ${config.version} - - org.opendaylight.controller - config-netconf-connector - ${netconf.version} - org.opendaylight.controller config-persister-api @@ -894,11 +881,6 @@ config-persister-feature-adapter ${config.version} - - org.opendaylight.controller - config-persister-impl - ${netconf.version} - org.opendaylight.controller @@ -1070,94 +1052,6 @@ ${dummy-console.version} - - - org.opendaylight.controller - netconf-api - ${netconf.version} - - - org.opendaylight.controller - netconf-client - ${netconf.version} - - - org.opendaylight.controller - netconf-client - ${netconf.version} - test-jar - - - - - org.opendaylight.controller - netconf-config-dispatcher - ${netconf.version} - - - org.opendaylight.controller - netconf-impl - ${netconf.version} - - - org.opendaylight.controller - netconf-impl - ${netconf.version} - test-jar - - - org.opendaylight.controller - netconf-mapping-api - ${netconf.version} - - - org.opendaylight.controller - netconf-monitoring - ${netconf.version} - - - org.opendaylight.controller - netconf-netty-util - ${netconf.version} - test-jar - - - org.opendaylight.controller - netconf-auth - ${netconf.version} - - - org.opendaylight.controller - netconf-usermanager - ${netconf.version} - - - org.opendaylight.controller - netconf-ssh - ${netconf.version} - - - org.opendaylight.controller - netconf-ssh - ${netconf.version} - test-jar - - - org.opendaylight.controller - netconf-tcp - ${netconf.version} - - - org.opendaylight.controller - netconf-util - ${netconf.version} - - - org.opendaylight.controller - netconf-util - ${netconf.version} - test-jar - org.opendaylight.controller netty-config-api @@ -1350,16 +1244,6 @@ md-sal-config ${mdsal.version} - - org.opendaylight.controller - netconf-config - ${netconf.version} - - - org.opendaylight.controller - netconf-connector-config - ${netconf.version} - org.opendaylight.controller sal-rest-docgen @@ -1691,11 +1575,7 @@ com.sun.jersey.jersey-servlet ${jersey-servlet.version} - - org.opendaylight.controller.thirdparty - ganymed - ${ganymed.version} - + org.opendaylight.controller.thirdparty @@ -1712,174 +1592,16 @@ org.openflow.openflowj 1.0.2 - - org.opendaylight.yangtools - binding-generator-impl - ${yangtools.version} - - - org.opendaylight.yangtools - binding-data-codec - ${yangtools.version} - - - org.opendaylight.yangtools - binding-generator-spi - ${yangtools.version} - - - org.opendaylight.yangtools - binding-generator-util - ${yangtools.version} - - - org.opendaylight.yangtools - binding-type-provider - ${yangtools.version} - - - org.opendaylight.yangtools - concepts - ${yangtools.version} - - - org.opendaylight.yangtools - object-cache-api - ${yangtools.version} - - - org.opendaylight.yangtools - object-cache-guava - ${yangtools.version} - - - org.opendaylight.yangtools - restconf-client-api - ${yangtools.version} - - - org.opendaylight.yangtools - restconf-client-impl - ${yangtools.version} - - - org.opendaylight.yangtools - util - ${yangtools.version} - - - org.opendaylight.yangtools - yang-data-composite-node - ${yangtools.version} - - - org.opendaylight.yangtools - yang-data-codec-gson - ${yangtools.version} - - - - org.opendaylight.yangtools - yang-binding - ${yangtools.version} - - - org.opendaylight.yangtools - yang-common - ${yangtools.version} - - - org.opendaylight.yangtools - yang-data-api - ${yangtools.version} - - - org.opendaylight.yangtools - yang-data-impl - ${yangtools.version} - + org.opendaylight.yangtools - yang-data-util + yangtools-artifacts ${yangtools.version} + pom + import - - org.opendaylight.yangtools - yang-maven-plugin-spi - ${yangtools.version} - - - org.opendaylight.yangtools - yang-model-api - ${yangtools.version} - - - org.opendaylight.yangtools - yang-model-util - ${yangtools.version} - - - org.opendaylight.yangtools - yang-parser-api - ${yangtools.version} - - - org.opendaylight.yangtools - yang-parser-impl - ${yangtools.version} - - - - org.opendaylight.yangtools.model - ietf-inet-types - ${ietf-inet-types.version} - - - org.opendaylight.yangtools.model - ietf-restconf - ${ietf-restconf.version} - - - org.opendaylight.yangtools.model - ietf-topology - ${ietf-topology.version} - - - org.opendaylight.yangtools.model - ietf-topology-l3-unicast-igp - ${ietf-topology.version} - - - org.opendaylight.yangtools.model - ietf-yang-types - ${ietf-yang-types.version} - - - org.opendaylight.yangtools.model - ietf-yang-types-20130715 - 2013.07.15.7-SNAPSHOT - - - org.opendaylight.yangtools.model - opendaylight-l2-types - ${opendaylight-l2-types.version} - - - org.opendaylight.yangtools.model - yang-ext - ${yang-ext.version} - - - org.opendaylight.yangtools.thirdparty - antlr4-runtime-osgi-nohead - 4.0 - - - org.opendaylight.yangtools.thirdparty - xtend-lib-osgi - ${xtend.version} - + org.openexi nagasena @@ -1950,12 +1672,6 @@ ${mdsal.version} test - - org.opendaylight.yangtools - mockito-configuration - ${yangtools.version} - test - org.opendaylight.controller features-config @@ -1972,14 +1688,6 @@ xml runtime - - org.opendaylight.controller - features-netconf - ${netconf.version} - features - xml - runtime - org.opendaylight.controller features-config-persister diff --git a/opendaylight/commons/protocol-framework/src/test/java/org/opendaylight/protocol/framework/ServerTest.java b/opendaylight/commons/protocol-framework/src/test/java/org/opendaylight/protocol/framework/ServerTest.java index 63026e384c..fc38888de3 100644 --- a/opendaylight/commons/protocol-framework/src/test/java/org/opendaylight/protocol/framework/ServerTest.java +++ b/opendaylight/commons/protocol-framework/src/test/java/org/opendaylight/protocol/framework/ServerTest.java @@ -250,52 +250,6 @@ public class ServerTest { assertFalse(session.isSuccess()); } - @Test - public void testNegotiationFailedNoReconnect() throws Exception { - final Promise p = new DefaultPromise<>(GlobalEventExecutor.INSTANCE); - - this.dispatcher = getServerDispatcher(p); - - this.server = this.dispatcher.createServer(this.serverAddress, new SessionListenerFactory() { - @Override - public SimpleSessionListener getSessionListener() { - return new SimpleSessionListener(); - } - }); - - this.server.get(); - - this.clientDispatcher = new SimpleDispatcher(new SessionNegotiatorFactory() { - @Override - public SessionNegotiator getSessionNegotiator(final SessionListenerFactory factory, - final Channel channel, final Promise promise) { - - return new SimpleSessionNegotiator(promise, channel) { - @Override - protected void startNegotiation() throws Exception { - negotiationFailed(new IllegalStateException("Negotiation failed")); - } - }; - } - }, new DefaultPromise(GlobalEventExecutor.INSTANCE), eventLoopGroup); - - final ReconnectStrategyFactory reconnectStrategyFactory = mock(ReconnectStrategyFactory.class); - final ReconnectStrategy reconnectStrategy = getMockedReconnectStrategy(); - doReturn(reconnectStrategy).when(reconnectStrategyFactory).createReconnectStrategy(); - - this.clientDispatcher.createReconnectingClient(this.serverAddress, - reconnectStrategyFactory, new SessionListenerFactory() { - @Override - public SimpleSessionListener getSessionListener() { - return new SimpleSessionListener(); - } - }); - - - // Only one strategy should be created for initial connect, no more = no reconnects - verify(reconnectStrategyFactory, times(1)).createReconnectStrategy(); - } - private SimpleDispatcher getClientDispatcher() { return new SimpleDispatcher(new SessionNegotiatorFactory() { @Override diff --git a/opendaylight/config/config-api/src/main/yang/config.yang b/opendaylight/config/config-api/src/main/yang/config.yang index 5d6c11fbee..e46d327ece 100644 --- a/opendaylight/config/config-api/src/main/yang/config.yang +++ b/opendaylight/config/config-api/src/main/yang/config.yang @@ -140,7 +140,7 @@ module config { "Top level container encapsulating configuration of all modules."; list module { - key "name"; + key "type name"; leaf name { description "Unique module instance name"; type string; diff --git a/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModuleFactory.java b/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModuleFactory.java index 4df9b036f1..1994e21a6d 100644 --- a/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModuleFactory.java +++ b/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownModuleFactory.java @@ -5,26 +5,16 @@ * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ -/** - * Generated file - - * Generated from: yang module name: shutdown-impl yang module local name: shutdown - * Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator - * Generated at: Wed Dec 18 14:02:06 CET 2013 - * - * Do not modify this file unless it is present under src/main directory - */ package org.opendaylight.controller.config.yang.shutdown.impl; +import java.util.Arrays; +import java.util.Set; import org.opendaylight.controller.config.api.DependencyResolver; import org.opendaylight.controller.config.api.DependencyResolverFactory; import org.opendaylight.controller.config.api.ModuleIdentifier; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import java.util.Arrays; -import java.util.Set; - public class ShutdownModuleFactory extends AbstractShutdownModuleFactory { public ShutdownModule instantiateModule(String instanceName, DependencyResolver dependencyResolver, diff --git a/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownServiceImpl.java b/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownServiceImpl.java index 4abbd3b36f..7d97fcd964 100644 --- a/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownServiceImpl.java +++ b/opendaylight/config/shutdown-impl/src/main/java/org/opendaylight/controller/config/yang/shutdown/impl/ShutdownServiceImpl.java @@ -8,15 +8,14 @@ package org.opendaylight.controller.config.yang.shutdown.impl; import com.google.common.base.Optional; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; import org.opendaylight.controller.config.shutdown.ShutdownService; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; - public class ShutdownServiceImpl implements ShutdownService, AutoCloseable { private final ShutdownService impl; private final ShutdownRuntimeRegistration registration; @@ -42,7 +41,7 @@ public class ShutdownServiceImpl implements ShutdownService, AutoCloseable { } class Impl implements ShutdownService { - private static final Logger logger = LoggerFactory.getLogger(Impl.class); + private static final Logger LOG = LoggerFactory.getLogger(Impl.class); private final String secret; private final Bundle systemBundle; @@ -53,27 +52,27 @@ class Impl implements ShutdownService { @Override public void shutdown(String inputSecret, Long maxWaitTime, Optional reason) { - logger.warn("Shutdown issued with secret {} and reason {}", inputSecret, reason); + LOG.warn("Shutdown issued with secret {} and reason {}", inputSecret, reason); try { Thread.sleep(1000); // prevent brute force attack } catch (InterruptedException e) { Thread.currentThread().interrupt(); - logger.warn("Shutdown process interrupted", e); + LOG.warn("Shutdown process interrupted", e); } if (this.secret.equals(inputSecret)) { - logger.info("Server is shutting down"); + LOG.info("Server is shutting down"); // actual work: Thread stopSystemBundleThread = new StopSystemBundleThread(systemBundle); stopSystemBundleThread.start(); if (maxWaitTime != null && maxWaitTime > 0) { Thread systemExitThread = new CallSystemExitThread(maxWaitTime); - logger.debug("Scheduling {}", systemExitThread); + LOG.debug("Scheduling {}", systemExitThread); systemExitThread.start(); } // end } else { - logger.warn("Unauthorized attempt to shut down server"); + LOG.warn("Unauthorized attempt to shut down server"); throw new IllegalArgumentException("Invalid secret"); } } @@ -81,7 +80,7 @@ class Impl implements ShutdownService { } class StopSystemBundleThread extends Thread { - private static final Logger logger = LoggerFactory.getLogger(StopSystemBundleThread.class); + private static final Logger LOG = LoggerFactory.getLogger(StopSystemBundleThread.class); private final Bundle systemBundle; StopSystemBundleThread(Bundle systemBundle) { @@ -94,18 +93,18 @@ class StopSystemBundleThread extends Thread { try { // wait so that JMX response is received Thread.sleep(1000); - logger.debug("Stopping system bundle"); + LOG.debug("Stopping system bundle"); systemBundle.stop(); } catch (BundleException e) { - logger.warn("Can not stop OSGi server", e); + LOG.warn("Can not stop OSGi server", e); } catch (InterruptedException e) { - logger.warn("Shutdown process interrupted", e); + LOG.warn("Shutdown process interrupted", e); } } } class CallSystemExitThread extends Thread { - private static final Logger logger = LoggerFactory.getLogger(CallSystemExitThread.class); + private static final Logger LOG = LoggerFactory.getLogger(CallSystemExitThread.class); private final long maxWaitTime; CallSystemExitThread(long maxWaitTime) { super("call-system-exit-daemon"); @@ -128,7 +127,7 @@ class CallSystemExitThread extends Thread { try { // wait specified time Thread.sleep(maxWaitTime); - logger.error("Since some threads are still running, server is going to shut down via System.exit(1) !"); + LOG.error("Since some threads are still running, server is going to shut down via System.exit(1) !"); // do a thread dump ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true); StringBuffer sb = new StringBuffer(); @@ -136,10 +135,10 @@ class CallSystemExitThread extends Thread { sb.append(info); sb.append("\n"); } - logger.warn("Thread dump:{}", sb); + LOG.warn("Thread dump:{}", sb); System.exit(1); } catch (InterruptedException e) { - logger.warn("Interrupted, not going to call System.exit(1)"); + LOG.warn("Interrupted, not going to call System.exit(1)"); } } } diff --git a/opendaylight/distribution/opendaylight/pom.xml b/opendaylight/distribution/opendaylight/pom.xml index e30ff05bf0..f6ecb44fa1 100644 --- a/opendaylight/distribution/opendaylight/pom.xml +++ b/opendaylight/distribution/opendaylight/pom.xml @@ -1152,10 +1152,6 @@ sample-toaster-provider ${mdsal.version} - - org.opendaylight.controller.thirdparty - ganymed - org.apache.sshd sshd-core @@ -1163,7 +1159,6 @@ org.opendaylight.yangtools binding-generator-api - ${yangtools.version} org.opendaylight.yangtools @@ -1184,7 +1179,6 @@ org.opendaylight.yangtools binding-model-api - ${yangtools.version} org.opendaylight.yangtools diff --git a/opendaylight/md-sal/sal-binding-it/src/main/java/org/opendaylight/controller/test/sal/binding/it/TestHelper.java b/opendaylight/md-sal/sal-binding-it/src/main/java/org/opendaylight/controller/test/sal/binding/it/TestHelper.java index efd35ccfa6..f7313f4ce7 100644 --- a/opendaylight/md-sal/sal-binding-it/src/main/java/org/opendaylight/controller/test/sal/binding/it/TestHelper.java +++ b/opendaylight/md-sal/sal-binding-it/src/main/java/org/opendaylight/controller/test/sal/binding/it/TestHelper.java @@ -76,7 +76,6 @@ public class TestHelper { mavenBundle("org.apache.sshd", "sshd-core").versionAsInProject(), // mavenBundle("org.openexi", "nagasena").versionAsInProject(), // mavenBundle("org.openexi", "nagasena-rta").versionAsInProject(), // - mavenBundle(CONTROLLER + ".thirdparty", "ganymed").versionAsInProject(), // mavenBundle(CONTROLLER, "netconf-mapping-api").versionAsInProject(), // mavenBundle(CONTROLLER, "config-persister-impl").versionAsInProject(), // diff --git a/opendaylight/md-sal/sal-dom-spi/src/main/java/org/opendaylight/controller/md/sal/dom/broker/spi/rpc/RpcRoutingStrategy.java b/opendaylight/md-sal/sal-dom-spi/src/main/java/org/opendaylight/controller/md/sal/dom/broker/spi/rpc/RpcRoutingStrategy.java index 81203c55fe..6c8f37b66b 100644 --- a/opendaylight/md-sal/sal-dom-spi/src/main/java/org/opendaylight/controller/md/sal/dom/broker/spi/rpc/RpcRoutingStrategy.java +++ b/opendaylight/md-sal/sal-dom-spi/src/main/java/org/opendaylight/controller/md/sal/dom/broker/spi/rpc/RpcRoutingStrategy.java @@ -7,6 +7,8 @@ */ package org.opendaylight.controller.md.sal.dom.broker.spi.rpc; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import org.opendaylight.yangtools.concepts.Identifiable; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; @@ -14,17 +16,14 @@ import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; import org.opendaylight.yangtools.yang.model.api.UnknownSchemaNode; -import com.google.common.base.Optional; - public abstract class RpcRoutingStrategy implements Identifiable { + private static final QName CONTEXT_REFERENCE = QName.cachedReference(QName.create("urn:opendaylight:yang:extension:yang-ext", + "2013-07-09", "context-reference")); private final QName identifier; - private static final QName CONTEXT_REFERENCE = QName.create("urn:opendaylight:yang:extension:yang-ext", - "2013-07-09", "context-reference"); private RpcRoutingStrategy(final QName identifier) { - super(); - this.identifier = identifier; + this.identifier = Preconditions.checkNotNull(identifier); } /** @@ -47,7 +46,7 @@ public abstract class RpcRoutingStrategy implements Identifiable { public abstract QName getContext(); @Override - public QName getIdentifier() { + public final QName getIdentifier() { return identifier; } @@ -64,14 +63,14 @@ public abstract class RpcRoutingStrategy implements Identifiable { for (DataSchemaNode schemaNode : input.getChildNodes()) { Optional context = getRoutingContext(schemaNode); if (context.isPresent()) { - return createRoutedStrategy(rpc, context.get(), schemaNode.getQName()); + return new RoutedRpcStrategy(rpc.getQName(), context.get(), schemaNode.getQName()); } } } - return createGlobalStrategy(rpc); + return new GlobalRpcStrategy(rpc.getQName()); } - public static Optional getRoutingContext(final DataSchemaNode schemaNode) { + public static Optional getRoutingContext(final DataSchemaNode schemaNode) { for (UnknownSchemaNode extension : schemaNode.getUnknownSchemaNodes()) { if (CONTEXT_REFERENCE.equals(extension.getNodeType())) { return Optional.fromNullable(extension.getQName()); @@ -80,26 +79,14 @@ public abstract class RpcRoutingStrategy implements Identifiable { return Optional.absent(); } - private static RpcRoutingStrategy createRoutedStrategy(final RpcDefinition rpc, final QName context, final QName leafNode) { - return new RoutedRpcStrategy(rpc.getQName(), context, leafNode); - } - - - - private static RpcRoutingStrategy createGlobalStrategy(final RpcDefinition rpc) { - GlobalRpcStrategy ret = new GlobalRpcStrategy(rpc.getQName()); - return ret; - } - - private static class RoutedRpcStrategy extends RpcRoutingStrategy { - - final QName context; + private static final class RoutedRpcStrategy extends RpcRoutingStrategy { + private final QName context; private final QName leaf; private RoutedRpcStrategy(final QName identifier, final QName ctx, final QName leaf) { super(identifier); - this.context = ctx; - this.leaf = leaf; + this.context = Preconditions.checkNotNull(ctx); + this.leaf = Preconditions.checkNotNull(leaf); } @Override @@ -118,7 +105,7 @@ public abstract class RpcRoutingStrategy implements Identifiable { } } - private static class GlobalRpcStrategy extends RpcRoutingStrategy { + private static final class GlobalRpcStrategy extends RpcRoutingStrategy { public GlobalRpcStrategy(final QName identifier) { super(identifier); @@ -131,12 +118,12 @@ public abstract class RpcRoutingStrategy implements Identifiable { @Override public QName getContext() { - throw new UnsupportedOperationException("Not routed strategy does not have context."); + throw new UnsupportedOperationException("Non-routed strategy does not have a context"); } @Override public QName getLeaf() { - throw new UnsupportedOperationException("Not routed strategy does not have context."); + throw new UnsupportedOperationException("Non-routed strategy does not have a context"); } } } \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.java index 3988a495cb..95fb9a4826 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.java @@ -7,9 +7,18 @@ */ package org.opendaylight.controller.sal.restconf.impl; +import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.CONFIGURATION; +import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL; + import com.google.common.base.Optional; import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import javax.ws.rs.core.Response.Status; import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope; import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; @@ -37,16 +46,6 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.core.Response.Status; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.CONFIGURATION; -import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL; - public class BrokerFacade { private final static Logger LOG = LoggerFactory.getLogger(BrokerFacade.class); @@ -261,15 +260,13 @@ public class BrokerFacade { try { - CheckedFuture future = - rwTx.exists(store, currentPath); + CheckedFuture future = rwTx.exists(store, currentPath); exists = future.checkedGet(); } catch (ReadFailedException e) { LOG.error("Failed to read pre-existing data from store {} path {}", store, currentPath, e); throw new IllegalStateException("Failed to read pre-existing data", e); } - if (!exists && iterator.hasNext()) { rwTx.merge(store, currentPath, currentOp.createDefault(currentArg)); } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java index a95a64b2c2..cd860efab7 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java @@ -9,9 +9,12 @@ package org.opendaylight.controller.sal.restconf.impl; import com.google.common.base.Objects; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Splitter; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -63,6 +66,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.InstanceI import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.tree.ModifiedNodeDoesNotExistException; import org.opendaylight.yangtools.yang.data.composite.node.schema.cnsn.parser.CnSnToNormalizedNodeParserFactory; import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode; import org.opendaylight.yangtools.yang.data.impl.NodeFactory; @@ -976,9 +980,13 @@ public class RestconfImpl implements RestconfService { broker.commitConfigurationDataDelete(normalizedII).get(); } } catch (Exception e) { - throw new RestconfDocumentedException("Error creating data", e); + final Optional searchedException = Iterables.tryFind(Throwables.getCausalChain(e), + Predicates.instanceOf(ModifiedNodeDoesNotExistException.class)); + if (searchedException.isPresent()) { + throw new RestconfDocumentedException("Data specified for deleting doesn't exist.", ErrorType.APPLICATION, ErrorTag.DATA_MISSING); + } + throw new RestconfDocumentedException("Error while deleting data", e); } - return Response.status(Status.OK).build(); } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java index 6b25830240..f533a6360a 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java @@ -8,9 +8,21 @@ package org.opendaylight.controller.sal.restconf.impl.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + import com.google.common.base.Optional; import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; +import java.util.concurrent.Future; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -43,19 +55,6 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdent import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.impl.schema.Builders; -import java.util.concurrent.Future; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - /** * Unit tests for BrokerFacade. * @@ -234,6 +233,9 @@ public class BrokerFacadeTest { when(wTransaction.submit()).thenReturn(expFuture); + NormalizedNode dummyNode2 = createDummyNode("dummy:namespace2", "2014-07-01", "dummy local name2"); + + CheckedFuture actualFuture = brokerFacade .commitConfigurationDataDelete(instanceID); diff --git a/opendaylight/netconf/netconf-artifacts/pom.xml b/opendaylight/netconf/netconf-artifacts/pom.xml new file mode 100644 index 0000000000..eb3cac18df --- /dev/null +++ b/opendaylight/netconf/netconf-artifacts/pom.xml @@ -0,0 +1,164 @@ + + + + + + 4.0.0 + org.opendaylight.controller + netconf-artifacts + 0.3.0-SNAPSHOT + pom + + + + + ${project.groupId} + netconf-config-dispatcher + ${project.version} + + + ${project.groupId} + config-netconf-connector + ${project.version} + + + ${project.groupId} + config-persister-impl + ${project.version} + + + ${project.groupId} + netconf-api + ${project.version} + + + ${project.groupId} + netconf-auth + ${project.version} + + + ${project.groupId} + netconf-cli + ${project.version} + + + ${project.groupId} + netconf-client + ${project.version} + + + ${project.groupId} + netconf-config + ${project.version} + + + ${project.groupId} + netconf-connector-config + ${project.version} + + + ${project.groupId} + netconf-impl + ${project.version} + + + ${project.groupId} + netconf-mapping-api + ${project.version} + + + ${project.groupId} + netconf-monitoring + ${project.version} + + + ${project.groupId} + netconf-netty-util + ${project.version} + + + ${project.groupId} + netconf-ssh + ${project.version} + + + ${project.groupId} + netconf-tcp + ${project.version} + + + ${project.groupId} + netconf-testtool + ${project.version} + + + ${project.groupId} + netconf-usermanager + ${project.version} + + + ${project.groupId} + netconf-util + ${project.version} + + + + ${project.groupId} + ietf-netconf-monitoring + ${project.version} + + + ${project.groupId} + ietf-netconf-monitoring-extension + ${project.version} + + + + ${project.groupId} + netconf-client + ${project.version} + test-jar + + + ${project.groupId} + netconf-impl + ${project.version} + test-jar + + + ${project.groupId} + netconf-netty-util + ${project.version} + test-jar + + + ${project.groupId} + netconf-ssh + ${project.version} + test-jar + + + ${project.groupId} + netconf-util + ${project.version} + test-jar + + + + ${project.groupId} + features-netconf + ${project.version} + features + xml + runtime + + + + + diff --git a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/SubtreeFilter.java b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/SubtreeFilter.java index 6e81584133..42a8bae448 100644 --- a/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/SubtreeFilter.java +++ b/opendaylight/netconf/netconf-impl/src/main/java/org/opendaylight/controller/netconf/impl/SubtreeFilter.java @@ -10,6 +10,7 @@ package org.opendaylight.controller.netconf.impl; import com.google.common.base.Optional; import java.io.IOException; +import java.util.Map; import org.opendaylight.controller.netconf.api.NetconfDocumentedException; import org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants; import org.opendaylight.controller.netconf.util.mapping.AbstractNetconfOperation.OperationNameAndNamespace; @@ -73,7 +74,7 @@ public class SubtreeFilter { return result; } - private static void addSubtree(XmlElement filter, XmlElement src, XmlElement dst) { + private static void addSubtree(XmlElement filter, XmlElement src, XmlElement dst) throws NetconfDocumentedException { for (XmlElement srcChild : src.getChildElements()) { for (XmlElement filterChild : filter.getChildElements()) { addSubtree2(filterChild, srcChild, dst); @@ -81,7 +82,7 @@ public class SubtreeFilter { } } - private static MatchingResult addSubtree2(XmlElement filter, XmlElement src, XmlElement dstParent) { + private static MatchingResult addSubtree2(XmlElement filter, XmlElement src, XmlElement dstParent) throws NetconfDocumentedException { Document document = dstParent.getDomElement().getOwnerDocument(); MatchingResult matches = matches(src, filter); if (matches != MatchingResult.NO_MATCH && matches != MatchingResult.CONTENT_MISMATCH) { @@ -123,7 +124,7 @@ public class SubtreeFilter { * Shallow compare src node to filter: tag name and namespace must match. * If filter node has no children and has text content, it also must match. */ - private static MatchingResult matches(XmlElement src, XmlElement filter) { + private static MatchingResult matches(XmlElement src, XmlElement filter) throws NetconfDocumentedException { boolean tagMatch = src.getName().equals(filter.getName()) && src.getNamespaceOptionally().equals(filter.getNamespaceOptionally()); MatchingResult result = null; @@ -131,7 +132,7 @@ public class SubtreeFilter { // match text content Optional maybeText = filter.getOnlyTextContentOptionally(); if (maybeText.isPresent()) { - if (maybeText.equals(src.getOnlyTextContentOptionally())) { + if (maybeText.equals(src.getOnlyTextContentOptionally()) || prefixedContentMatches(filter, src)) { result = MatchingResult.CONTENT_MATCH; } else { result = MatchingResult.CONTENT_MISMATCH; @@ -159,10 +160,30 @@ public class SubtreeFilter { if (result == null) { result = MatchingResult.NO_MATCH; } - logger.debug("Matching {} to {} resulted in {}", src, filter, tagMatch); + logger.debug("Matching {} to {} resulted in {}", src, filter, result); return result; } + private static boolean prefixedContentMatches(final XmlElement filter, final XmlElement src) throws NetconfDocumentedException { + final Map.Entry prefixToNamespaceOfFilter = filter.findNamespaceOfTextContent(); + final Map.Entry prefixToNamespaceOfSrc = src.findNamespaceOfTextContent(); + + final String prefix = prefixToNamespaceOfFilter.getKey(); + // If this is not a prefixed content, we do not need to continue since content do not match + if(prefix.equals(XmlElement.DEFAULT_NAMESPACE_PREFIX)) { + return false; + } + // Namespace mismatch + if(!prefixToNamespaceOfFilter.getValue().equals(prefixToNamespaceOfSrc.getValue())) { + return false; + } + + final String unprefixedFilterContent = filter.getTextContent().substring(prefix.length()); + final String unprefixedSrcCOntnet = src.getTextContent().substring(prefix.length()); + // Finally compare unprefixed content + return unprefixedFilterContent.equals(unprefixedSrcCOntnet); + } + enum MatchingResult { NO_MATCH, TAG_MATCH, CONTENT_MATCH, CONTENT_MISMATCH } diff --git a/opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/SubtreeFilterTest.java b/opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/SubtreeFilterTest.java index b11834386e..5d9470750e 100644 --- a/opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/SubtreeFilterTest.java +++ b/opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/SubtreeFilterTest.java @@ -36,7 +36,7 @@ public class SubtreeFilterTest { @Parameters public static Collection data() { List result = new ArrayList<>(); - for (int i = 0; i <= 8; i++) { + for (int i = 0; i <= 9; i++) { result.add(new Object[]{i}); } return result; diff --git a/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/post-filter.xml b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/post-filter.xml new file mode 100644 index 0000000000..afe9655326 --- /dev/null +++ b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/post-filter.xml @@ -0,0 +1,14 @@ + + + + + + fred + x:admin + Fred Flintstone + + + + + \ No newline at end of file diff --git a/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/pre-filter.xml b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/pre-filter.xml new file mode 100644 index 0000000000..eca3241f05 --- /dev/null +++ b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/pre-filter.xml @@ -0,0 +1,40 @@ + + + + + + root + superuser + Charlie Root + + 1 + 1 + + + + fred + x:admin + Fred Flintstone + + 2 + 2 + + + + barney + admin + Barney Rubble + + 2 + 3 + + + + + + admin + + + + + \ No newline at end of file diff --git a/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/request.xml b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/request.xml new file mode 100644 index 0000000000..47da0feec1 --- /dev/null +++ b/opendaylight/netconf/netconf-impl/src/test/resources/subtree/9/request.xml @@ -0,0 +1,19 @@ + + + + + + + + + + fred + a:admin + + + + + + + \ No newline at end of file diff --git a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java index 1adcd7e491..f96f557619 100644 --- a/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java +++ b/opendaylight/netconf/netconf-it/src/test/java/org/opendaylight/controller/netconf/it/NetconfITSecureTest.java @@ -21,14 +21,23 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import io.netty.channel.EventLoopGroup; import io.netty.channel.local.LocalAddress; +import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.GlobalEventExecutor; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.file.Files; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -40,13 +49,12 @@ import org.opendaylight.controller.netconf.client.NetconfClientDispatcher; import org.opendaylight.controller.netconf.client.NetconfClientDispatcherImpl; import org.opendaylight.controller.netconf.client.NetconfClientSessionListener; import org.opendaylight.controller.netconf.client.SimpleNetconfClientSessionListener; +import org.opendaylight.controller.netconf.client.TestingNetconfClient; import org.opendaylight.controller.netconf.client.conf.NetconfClientConfiguration; import org.opendaylight.controller.netconf.client.conf.NetconfClientConfigurationBuilder; -import org.opendaylight.controller.netconf.client.TestingNetconfClient; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.LoginPassword; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.controller.netconf.util.messages.NetconfMessageUtil; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.opendaylight.controller.netconf.util.xml.XmlUtil; @@ -68,19 +76,32 @@ public class NetconfITSecureTest extends AbstractNetconfConfigTest { public static final String USERNAME = "user"; public static final String PASSWORD = "pwd"; - private NetconfSSHServer sshServer; + private SshProxyServer sshProxyServer; + + private ExecutorService nioExec; + private EventLoopGroup clientGroup; + private ScheduledExecutorService minaTimerEx; @Before public void setUp() throws Exception { - final char[] pem = PEMGenerator.generate().toCharArray(); - sshServer = NetconfSSHServer.start(TLS_ADDRESS.getPort(), NetconfConfigUtil.getNetconfLocalAddress(), getNettyThreadgroup(), pem); - sshServer.setAuthProvider(getAuthProvider()); + nioExec = Executors.newFixedThreadPool(1); + clientGroup = new NioEventLoopGroup(); + minaTimerEx = Executors.newScheduledThreadPool(1); + sshProxyServer = new SshProxyServer(minaTimerEx, clientGroup, nioExec); + sshProxyServer.bind(TLS_ADDRESS, NetconfConfigUtil.getNetconfLocalAddress(), new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return true; + } + }, new PEMGeneratorHostKeyProvider(Files.createTempFile("prefix", "suffix").toAbsolutePath().toString())); } @After public void tearDown() throws Exception { - sshServer.close(); - sshServer.join(); + sshProxyServer.close(); + clientGroup.shutdownGracefully().await(); + minaTimerEx.shutdownNow(); + nioExec.shutdownNow(); } @Test diff --git a/opendaylight/netconf/netconf-netty-util/pom.xml b/opendaylight/netconf/netconf-netty-util/pom.xml index e2afcc42f5..a9c1e8336d 100644 --- a/opendaylight/netconf/netconf-netty-util/pom.xml +++ b/opendaylight/netconf/netconf-netty-util/pom.xml @@ -48,10 +48,6 @@ org.opendaylight.controller protocol-framework - - org.opendaylight.controller.thirdparty - ganymed - org.apache.sshd sshd-core @@ -89,7 +85,7 @@ maven-bundle-plugin - org.apache.sshd.*, ch.ethz.ssh2, com.google.common.base, com.google.common.collect, io.netty.buffer, + org.apache.sshd.*, com.google.common.base, com.google.common.collect, io.netty.buffer, io.netty.channel, io.netty.channel.socket, io.netty.handler.codec, io.netty.handler.ssl, io.netty.util, io.netty.util.concurrent, javax.xml.transform, javax.xml.transform.dom, javax.xml.transform.sax, javax.xml.transform.stream, org.opendaylight.controller.netconf.api, diff --git a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java index 3bd7232023..fa7d0900ed 100644 --- a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java +++ b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java @@ -8,12 +8,9 @@ package org.opendaylight.controller.netconf.nettyutil.handler.ssh.client; -import com.google.common.base.Preconditions; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; import java.io.IOException; import java.net.SocketAddress; + import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; import org.apache.sshd.SshClient; @@ -26,6 +23,13 @@ import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication. import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Preconditions; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + /** * Netty SSH handler class. Acts as interface between Netty and SSH library. */ @@ -47,7 +51,7 @@ public class AsyncSshHandler extends ChannelOutboundHandlerAdapter { private final AuthenticationHandler authenticationHandler; private final SshClient sshClient; - private AsyncSshHanderReader sshReadAsyncListener; + private AsyncSshHandlerReader sshReadAsyncListener; private AsyncSshHandlerWriter sshWriteAsyncHandler; private ClientChannel channel; @@ -138,7 +142,20 @@ public class AsyncSshHandler extends ChannelOutboundHandlerAdapter { connectPromise.setSuccess(); connectPromise = null; - sshReadAsyncListener = new AsyncSshHanderReader(this, ctx, channel.getAsyncOut()); + // TODO we should also read from error stream and at least log from that + + sshReadAsyncListener = new AsyncSshHandlerReader(new AutoCloseable() { + @Override + public void close() throws Exception { + AsyncSshHandler.this.disconnect(ctx, ctx.newPromise()); + } + }, new AsyncSshHandlerReader.ReadMsgHandler() { + @Override + public void onMessageRead(final ByteBuf msg) { + ctx.fireChannelRead(msg); + } + }, channel.toString(), channel.getAsyncOut()); + // if readAsyncListener receives immediate close, it will close this handler and closing this handler sets channel variable to null if(channel != null) { sshWriteAsyncHandler = new AsyncSshHandlerWriter(channel.getAsyncIn()); diff --git a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHanderReader.java b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java similarity index 66% rename from opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHanderReader.java rename to opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java index 73a24f27b2..ada15583cd 100644 --- a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHanderReader.java +++ b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java @@ -8,9 +8,8 @@ package org.opendaylight.controller.netconf.nettyutil.handler.ssh.client; +import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandler; import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.io.IoInputStream; import org.apache.sshd.common.io.IoReadFuture; @@ -22,22 +21,24 @@ import org.slf4j.LoggerFactory; * Listener on async input stream from SSH session. * This listeners schedules reads in a loop until the session is closed or read fails. */ -final class AsyncSshHanderReader implements SshFutureListener, AutoCloseable { +public final class AsyncSshHandlerReader implements SshFutureListener, AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(AsyncSshHandler.class); private static final int BUFFER_SIZE = 8192; - private final ChannelOutboundHandler asyncSshHandler; - private final ChannelHandlerContext ctx; + private final AutoCloseable connectionClosedCallback; + private final ReadMsgHandler readHandler; + private final String channelId; private IoInputStream asyncOut; private Buffer buf; private IoReadFuture currentReadFuture; - public AsyncSshHanderReader(final ChannelOutboundHandler asyncSshHandler, final ChannelHandlerContext ctx, final IoInputStream asyncOut) { - this.asyncSshHandler = asyncSshHandler; - this.ctx = ctx; + public AsyncSshHandlerReader(final AutoCloseable connectionClosedCallback, final ReadMsgHandler readHandler, final String channelId, final IoInputStream asyncOut) { + this.connectionClosedCallback = connectionClosedCallback; + this.readHandler = readHandler; + this.channelId = channelId; this.asyncOut = asyncOut; buf = new Buffer(BUFFER_SIZE); asyncOut.read(buf).addListener(this); @@ -48,16 +49,20 @@ final class AsyncSshHanderReader implements SshFutureListener, Aut if(future.getException() != null) { if(asyncOut.isClosed() || asyncOut.isClosing()) { // Ssh dropped - logger.debug("Ssh session dropped on channel: {}", ctx.channel(), future.getException()); + logger.debug("Ssh session dropped on channel: {}", channelId, future.getException()); } else { - logger.warn("Exception while reading from SSH remote on channel {}", ctx.channel(), future.getException()); + logger.warn("Exception while reading from SSH remote on channel {}", channelId, future.getException()); } invokeDisconnect(); return; } if (future.getRead() > 0) { - ctx.fireChannelRead(Unpooled.wrappedBuffer(buf.array(), 0, future.getRead())); + final ByteBuf msg = Unpooled.wrappedBuffer(buf.array(), 0, future.getRead()); + if(logger.isTraceEnabled()) { + logger.trace("Reading message on channel: {}, message: {}", channelId, AsyncSshHandlerWriter.byteBufToString(msg)); + } + readHandler.onMessageRead(msg); // Schedule next read buf = new Buffer(BUFFER_SIZE); @@ -68,7 +73,7 @@ final class AsyncSshHanderReader implements SshFutureListener, Aut private void invokeDisconnect() { try { - asyncSshHandler.disconnect(ctx, ctx.newPromise()); + connectionClosedCallback.close(); } catch (final Exception e) { // This should not happen throw new IllegalStateException(e); @@ -80,8 +85,14 @@ final class AsyncSshHanderReader implements SshFutureListener, Aut // Remove self as listener on close to prevent reading from closed input if(currentReadFuture != null) { currentReadFuture.removeListener(this); + currentReadFuture = null; } asyncOut = null; } + + public interface ReadMsgHandler { + + void onMessageRead(ByteBuf msg); + } } diff --git a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java index eace0ac7ea..8e639bd47c 100644 --- a/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java +++ b/opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java @@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory; * Async Ssh writer. Takes messages(byte arrays) and sends them encrypted to remote server. * Also handles pending writes by caching requests until pending state is over. */ -final class AsyncSshHandlerWriter implements AutoCloseable { +public final class AsyncSshHandlerWriter implements AutoCloseable { private static final Logger logger = LoggerFactory .getLogger(AsyncSshHandlerWriter.class); @@ -116,7 +116,7 @@ final class AsyncSshHandlerWriter implements AutoCloseable { writeWithPendingDetection(pendingWrite.ctx, pendingWrite.promise, msg); } - private static String byteBufToString(final ByteBuf msg) { + public static String byteBufToString(final ByteBuf msg) { msg.resetReaderIndex(); final String s = msg.toString(Charsets.UTF_8); msg.resetReaderIndex(); diff --git a/opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java b/opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java index d0fc43d04a..212eabb290 100644 --- a/opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java +++ b/opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java @@ -459,6 +459,8 @@ public class AsyncSshHandlerTest { private ChannelSubsystem getMockedSubsystemChannel(final IoInputStream asyncOut, final IoOutputStream asyncIn) throws IOException { final ChannelSubsystem subsystemChannel = mock(ChannelSubsystem.class); + doReturn("subsystemChannel").when(subsystemChannel).toString(); + doNothing().when(subsystemChannel).setStreaming(any(ClientChannel.Streaming.class)); final OpenFuture openFuture = mock(OpenFuture.class); diff --git a/opendaylight/netconf/netconf-ssh/pom.xml b/opendaylight/netconf/netconf-ssh/pom.xml index 221626b741..e0c7dba4fa 100644 --- a/opendaylight/netconf/netconf-ssh/pom.xml +++ b/opendaylight/netconf/netconf-ssh/pom.xml @@ -36,10 +36,6 @@ org.bouncycastle bcprov-jdk15on - - org.opendaylight.controller.thirdparty - ganymed - org.apache.sshd sshd-core @@ -60,7 +56,6 @@ org.opendaylight.controller netconf-netty-util - test org.opendaylight.controller diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java deleted file mode 100644 index 86206a7d5c..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/NetconfSSHServer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.controller.netconf.ssh; - -import com.google.common.base.Preconditions; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; - -import javax.annotation.concurrent.ThreadSafe; - -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.ssh.threads.Handshaker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; - -import io.netty.channel.EventLoopGroup; -import io.netty.channel.local.LocalAddress; - -/** - * Thread that accepts client connections. Accepted socket is forwarded to {@link org.opendaylight.controller.netconf.ssh.threads.Handshaker}, - * which is executed in {@link #handshakeExecutor}. - */ -@ThreadSafe -public final class NetconfSSHServer extends Thread implements AutoCloseable { - - private static final Logger logger = LoggerFactory.getLogger(NetconfSSHServer.class); - private static final AtomicLong sessionIdCounter = new AtomicLong(); - - private final ServerSocket serverSocket; - private final LocalAddress localAddress; - private final EventLoopGroup bossGroup; - private Optional authProvider = Optional.absent(); - private final ExecutorService handshakeExecutor; - private final char[] pem; - private volatile boolean up; - - private NetconfSSHServer(final int serverPort, final LocalAddress localAddress, final EventLoopGroup bossGroup, final char[] pem) throws IOException { - super(NetconfSSHServer.class.getSimpleName()); - this.bossGroup = bossGroup; - this.pem = pem; - logger.trace("Creating SSH server socket on port {}", serverPort); - this.serverSocket = new ServerSocket(serverPort); - if (serverSocket.isBound() == false) { - throw new IllegalStateException("Socket can't be bound to requested port :" + serverPort); - } - logger.trace("Server socket created."); - this.localAddress = localAddress; - this.up = true; - handshakeExecutor = Executors.newFixedThreadPool(10); - } - - public static NetconfSSHServer start(final int serverPort, final LocalAddress localAddress, final EventLoopGroup bossGroup, final char[] pemArray) throws IOException { - final NetconfSSHServer netconfSSHServer = new NetconfSSHServer(serverPort, localAddress, bossGroup, pemArray); - netconfSSHServer.start(); - return netconfSSHServer; - } - - public synchronized AuthProvider getAuthProvider() { - Preconditions.checkState(authProvider.isPresent(), "AuthenticationProvider is not set up, cannot authenticate user"); - return authProvider.get(); - } - - public synchronized void setAuthProvider(final AuthProvider authProvider) { - if(this.authProvider != null) { - logger.debug("Changing auth provider to {}", authProvider); - } - this.authProvider = Optional.fromNullable(authProvider); - } - - @Override - public void close() throws IOException { - up = false; - logger.trace("Closing SSH server socket."); - serverSocket.close(); - bossGroup.shutdownGracefully(); - logger.trace("SSH server socket closed."); - } - - @VisibleForTesting - public InetSocketAddress getLocalSocketAddress() { - return (InetSocketAddress) serverSocket.getLocalSocketAddress(); - } - - @Override - public void run() { - while (up) { - Socket acceptedSocket = null; - try { - acceptedSocket = serverSocket.accept(); - } catch (final IOException e) { - if (up == false) { - logger.trace("Exiting server thread", e); - } else { - logger.warn("Exception occurred during socket.accept", e); - } - } - if (acceptedSocket != null) { - try { - final Handshaker task = new Handshaker(acceptedSocket, localAddress, sessionIdCounter.incrementAndGet(), getAuthProvider(), bossGroup, pem); - handshakeExecutor.submit(task); - } catch (final IOException e) { - logger.warn("Cannot set PEMHostKey, closing connection", e); - closeSocket(acceptedSocket); - } catch (final IllegalStateException e) { - logger.warn("Cannot accept connection, closing", e); - closeSocket(acceptedSocket); - } - } - } - logger.debug("Server thread is exiting"); - } - - private void closeSocket(final Socket acceptedSocket) { - try { - acceptedSocket.close(); - } catch (final IOException e) { - logger.warn("Ignoring exception while closing socket", e); - } - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/RemoteNetconfCommand.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/RemoteNetconfCommand.java new file mode 100644 index 0000000000..e642e073a3 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/RemoteNetconfCommand.java @@ -0,0 +1,191 @@ +/* + * 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.ssh; + +import com.google.common.base.Preconditions; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +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.util.concurrent.GenericFutureListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.io.IoInputStream; +import org.apache.sshd.common.io.IoOutputStream; +import org.apache.sshd.server.AsyncCommand; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; +import org.opendaylight.controller.netconf.util.messages.NetconfHelloMessageAdditionalHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This command handles all netconf related rpc and forwards to delegate server. + * Uses netty to make a local connection to delegate server. + * + * Command is Apache Mina SSH terminology for objects handling ssh data. + */ +public class RemoteNetconfCommand implements AsyncCommand, SessionAware { + + private static final Logger logger = LoggerFactory.getLogger(RemoteNetconfCommand.class); + + private final EventLoopGroup clientEventGroup; + private final LocalAddress localAddress; + + private IoInputStream in; + private IoOutputStream out; + private ExitCallback callback; + private NetconfHelloMessageAdditionalHeader netconfHelloMessageAdditionalHeader; + + private Channel clientChannel; + private ChannelFuture clientChannelFuture; + + public RemoteNetconfCommand(final EventLoopGroup clientEventGroup, final LocalAddress localAddress) { + this.clientEventGroup = clientEventGroup; + this.localAddress = localAddress; + } + + @Override + public void setIoInputStream(final IoInputStream in) { + this.in = in; + } + + @Override + public void setIoOutputStream(final IoOutputStream out) { + this.out = out; + } + + @Override + public void setIoErrorStream(final IoOutputStream err) { + // TODO do we want to use error stream in some way ? + } + + @Override + public void setInputStream(final InputStream in) { + throw new UnsupportedOperationException("Synchronous IO is unsupported"); + } + + @Override + public void setOutputStream(final OutputStream out) { + throw new UnsupportedOperationException("Synchronous IO is unsupported"); + + } + + @Override + public void setErrorStream(final OutputStream err) { + throw new UnsupportedOperationException("Synchronous IO is unsupported"); + + } + + @Override + public void setExitCallback(final ExitCallback callback) { + this.callback = callback; + } + + @Override + public void start(final Environment env) throws IOException { + logger.trace("Establishing internal connection to netconf server for client: {}", getClientAddress()); + + final Bootstrap clientBootstrap = new Bootstrap(); + clientBootstrap.group(clientEventGroup).channel(LocalChannel.class); + + clientBootstrap + .handler(new ChannelInitializer() { + @Override + public void initChannel(final LocalChannel ch) throws Exception { + ch.pipeline().addLast(new SshProxyClientHandler(in, out, netconfHelloMessageAdditionalHeader, callback)); + } + }); + clientChannelFuture = clientBootstrap.connect(localAddress); + clientChannelFuture.addListener(new GenericFutureListener() { + + @Override + public void operationComplete(final ChannelFuture future) throws Exception { + if(future.isSuccess()) { + clientChannel = clientChannelFuture.channel(); + } else { + logger.warn("Unable to establish internal connection to netconf server for client: {}", getClientAddress()); + Preconditions.checkNotNull(callback, "Exit callback must be set"); + callback.onExit(1, "Unable to establish internal connection to netconf server for client: "+ getClientAddress()); + } + } + }); + } + + @Override + public void destroy() { + logger.trace("Releasing internal connection to netconf server for client: {} on channel: {}", + getClientAddress(), clientChannel); + + clientChannelFuture.cancel(true); + if(clientChannel != null) { + clientChannel.close().addListener(new GenericFutureListener() { + + @Override + public void operationComplete(final ChannelFuture future) throws Exception { + if (future.isSuccess() == false) { + logger.warn("Unable to release internal connection to netconf server on channel: {}", clientChannel); + } + } + }); + } + } + + private String getClientAddress() { + return netconfHelloMessageAdditionalHeader.getAddress(); + } + + @Override + public void setSession(final ServerSession session) { + final SocketAddress remoteAddress = session.getIoSession().getRemoteAddress(); + String hostName = ""; + String port = ""; + if(remoteAddress instanceof InetSocketAddress) { + hostName = ((InetSocketAddress) remoteAddress).getAddress().getHostAddress(); + port = Integer.toString(((InetSocketAddress) remoteAddress).getPort()); + } + netconfHelloMessageAdditionalHeader = new NetconfHelloMessageAdditionalHeader( + session.getUsername(), hostName, port, "ssh", "client"); + } + + public static class NetconfCommandFactory implements NamedFactory { + + public static final String NETCONF = "netconf"; + + private final EventLoopGroup clientBootstrap; + private final LocalAddress localAddress; + + public NetconfCommandFactory(final EventLoopGroup clientBootstrap, final LocalAddress localAddress) { + + this.clientBootstrap = clientBootstrap; + this.localAddress = localAddress; + } + + @Override + public String getName() { + return NETCONF; + } + + @Override + public RemoteNetconfCommand create() { + return new RemoteNetconfCommand(clientBootstrap, localAddress); + } + } + +} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyClientHandler.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyClientHandler.java new file mode 100644 index 0000000000..2b2b3b3e81 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyClientHandler.java @@ -0,0 +1,99 @@ +/* + * 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.ssh; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.apache.sshd.common.io.IoInputStream; +import org.apache.sshd.common.io.IoOutputStream; +import org.apache.sshd.server.ExitCallback; +import org.opendaylight.controller.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerReader; +import org.opendaylight.controller.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerWriter; +import org.opendaylight.controller.netconf.util.messages.NetconfHelloMessageAdditionalHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Netty handler that reads SSH from remote client and writes to delegate server and reads from delegate server and writes to remote client + */ +final class SshProxyClientHandler extends ChannelInboundHandlerAdapter { + + private static final Logger logger = LoggerFactory.getLogger(SshProxyClientHandler.class); + + private final IoInputStream in; + private final IoOutputStream out; + + private AsyncSshHandlerReader asyncSshHandlerReader; + private AsyncSshHandlerWriter asyncSshHandlerWriter; + + private final NetconfHelloMessageAdditionalHeader netconfHelloMessageAdditionalHeader; + private final ExitCallback callback; + + public SshProxyClientHandler(final IoInputStream in, final IoOutputStream out, + final NetconfHelloMessageAdditionalHeader netconfHelloMessageAdditionalHeader, + final ExitCallback callback) { + this.in = in; + this.out = out; + this.netconfHelloMessageAdditionalHeader = netconfHelloMessageAdditionalHeader; + this.callback = callback; + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + writeAdditionalHeader(ctx); + + asyncSshHandlerWriter = new AsyncSshHandlerWriter(out); + asyncSshHandlerReader = new AsyncSshHandlerReader(new AutoCloseable() { + @Override + public void close() throws Exception { + // Close both sessions (delegate server and remote client) + ctx.fireChannelInactive(); + ctx.disconnect(); + ctx.close(); + asyncSshHandlerReader.close(); + asyncSshHandlerWriter.close(); + } + }, new AsyncSshHandlerReader.ReadMsgHandler() { + @Override + public void onMessageRead(final ByteBuf msg) { + if(logger.isTraceEnabled()) { + logger.trace("Forwarding message for client: {} on channel: {}, message: {}", + netconfHelloMessageAdditionalHeader.getAddress(), ctx.channel(), AsyncSshHandlerWriter.byteBufToString(msg)); + } + // Just forward to delegate + ctx.writeAndFlush(msg); + } + }, "ssh" + netconfHelloMessageAdditionalHeader.getAddress(), in); + + + super.channelActive(ctx); + } + + private void writeAdditionalHeader(final ChannelHandlerContext ctx) { + ctx.writeAndFlush(Unpooled.copiedBuffer(netconfHelloMessageAdditionalHeader.toFormattedString().getBytes())); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { + asyncSshHandlerWriter.write(ctx, msg, ctx.newPromise()); + } + + @Override + public void channelInactive(final ChannelHandlerContext ctx) throws Exception { + logger.debug("Internal connection to netconf server was dropped for client: {} on channel: ", + netconfHelloMessageAdditionalHeader.getAddress(), ctx.channel()); + callback.onExit(1, "Internal connection to netconf server was dropped for client: " + + netconfHelloMessageAdditionalHeader.getAddress() + " on channel: " + ctx.channel()); + super.channelInactive(ctx); + } + + +} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyServer.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyServer.java new file mode 100644 index 0000000000..0b85cf2653 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/SshProxyServer.java @@ -0,0 +1,134 @@ +/* + * 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.ssh; + +import com.google.common.collect.Lists; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.AsynchronousChannelGroup; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.sshd.SshServer; +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.RuntimeSshException; +import org.apache.sshd.common.io.IoAcceptor; +import org.apache.sshd.common.io.IoConnector; +import org.apache.sshd.common.io.IoHandler; +import org.apache.sshd.common.io.IoServiceFactory; +import org.apache.sshd.common.io.IoServiceFactoryFactory; +import org.apache.sshd.common.io.nio2.Nio2Acceptor; +import org.apache.sshd.common.io.nio2.Nio2Connector; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; +import org.apache.sshd.common.util.CloseableUtils; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.PasswordAuthenticator; + +/** + * Proxy SSH server that just delegates decrypted content to a delegate server within same VM. + * Implemented using Apache Mina SSH lib. + */ +public class SshProxyServer implements AutoCloseable { + + private final SshServer sshServer; + private final ScheduledExecutorService minaTimerExecutor; + private final EventLoopGroup clientGroup; + private final IoServiceFactoryFactory nioServiceWithPoolFactoryFactory; + + public SshProxyServer(final ScheduledExecutorService minaTimerExecutor, final EventLoopGroup clientGroup, final ExecutorService nioExecutor) { + this.minaTimerExecutor = minaTimerExecutor; + this.clientGroup = clientGroup; + this.nioServiceWithPoolFactoryFactory = new NioServiceWithPoolFactory.NioServiceWithPoolFactoryFactory(nioExecutor); + this.sshServer = SshServer.setUpDefaultServer(); + } + + public void bind(final InetSocketAddress bindingAddress, final LocalAddress localAddress, final PasswordAuthenticator authenticator, final KeyPairProvider keyPairProvider) throws IOException { + sshServer.setHost(bindingAddress.getHostString()); + sshServer.setPort(bindingAddress.getPort()); + + sshServer.setPasswordAuthenticator(authenticator); + sshServer.setKeyPairProvider(keyPairProvider); + + sshServer.setIoServiceFactoryFactory(nioServiceWithPoolFactoryFactory); + sshServer.setScheduledExecutorService(minaTimerExecutor); + + final RemoteNetconfCommand.NetconfCommandFactory netconfCommandFactory = + new RemoteNetconfCommand.NetconfCommandFactory(clientGroup, localAddress); + sshServer.setSubsystemFactories(Lists.>newArrayList(netconfCommandFactory)); + sshServer.start(); + } + + @Override + public void close() { + try { + sshServer.stop(true); + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted while stopping sshServer", e); + } finally { + sshServer.close(true); + } + } + + /** + * Based on Nio2ServiceFactory with one addition: injectable executor + */ + private static final class NioServiceWithPoolFactory extends CloseableUtils.AbstractCloseable implements IoServiceFactory { + + private final FactoryManager manager; + private final AsynchronousChannelGroup group; + + public NioServiceWithPoolFactory(final FactoryManager manager, final ExecutorService executor) { + this.manager = manager; + try { + group = AsynchronousChannelGroup.withThreadPool(executor); + } catch (final IOException e) { + throw new RuntimeSshException(e); + } + } + + public IoConnector createConnector(final IoHandler handler) { + return new Nio2Connector(manager, handler, group); + } + + public IoAcceptor createAcceptor(final IoHandler handler) { + return new Nio2Acceptor(manager, handler, group); + } + + @Override + protected void doCloseImmediately() { + try { + group.shutdownNow(); + group.awaitTermination(5, TimeUnit.SECONDS); + } catch (final Exception e) { + log.debug("Exception caught while closing channel group", e); + } finally { + super.doCloseImmediately(); + } + } + + private static final class NioServiceWithPoolFactoryFactory extends Nio2ServiceFactoryFactory { + + private final ExecutorService nioExecutor; + + private NioServiceWithPoolFactoryFactory(final ExecutorService nioExecutor) { + this.nioExecutor = nioExecutor; + } + + @Override + public IoServiceFactory create(final FactoryManager manager) { + return new NioServiceWithPoolFactory(manager, nioExecutor); + } + } + } + +} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java deleted file mode 100644 index 53ab8219ee..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/authentication/PEMGenerator.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ - -package org.opendaylight.controller.netconf.ssh.authentication; - -import com.google.common.annotations.VisibleForTesting; -import java.io.FileInputStream; -import java.security.NoSuchAlgorithmException; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.bouncycastle.openssl.PEMWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.io.StringWriter; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; - -public class PEMGenerator { - private static final Logger logger = LoggerFactory.getLogger(PEMGenerator.class); - private static final int KEY_SIZE = 4096; - - - public static String readOrGeneratePK(File privateKeyFile) throws IOException { - if (privateKeyFile.exists() == false) { - // generate & save to file - try { - return generateTo(privateKeyFile); - } catch (Exception e) { - logger.error("Exception occurred while generating PEM string to {}", privateKeyFile, e); - throw new IllegalStateException("Error generating RSA key from file " + privateKeyFile); - } - } else { - // read from file - try (FileInputStream fis = new FileInputStream(privateKeyFile)) { - return IOUtils.toString(fis); - } catch (final IOException e) { - logger.error("Error reading RSA key from file {}", privateKeyFile, e); - throw new IOException("Error reading RSA key from file " + privateKeyFile, e); - } - } - } - - /** - * Generate private key to a file and return its content as string. - * - * @param privateFile path where private key should be generated - * @return String representation of private key - * @throws IOException - * @throws NoSuchAlgorithmException - */ - @VisibleForTesting - public static String generateTo(File privateFile) throws IOException, NoSuchAlgorithmException { - logger.info("Generating private key to {}", privateFile.getAbsolutePath()); - String privatePEM = generate(); - FileUtils.write(privateFile, privatePEM); - return privatePEM; - } - - @VisibleForTesting - public static String generate() throws NoSuchAlgorithmException, IOException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); - SecureRandom sr = new SecureRandom(); - keyGen.initialize(KEY_SIZE, sr); - KeyPair keypair = keyGen.generateKeyPair(); - return toString(keypair.getPrivate()); - } - - /** - * Get string representation of a key. - */ - private static String toString(Key key) throws IOException { - try (StringWriter writer = new StringWriter()) { - try (PEMWriter pemWriter = new PEMWriter(writer)) { - pemWriter.writeObject(key); - } - return writer.toString(); - } - } - -} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/AuthProviderTracker.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/AuthProviderTracker.java new file mode 100644 index 0000000000..97e611c0d2 --- /dev/null +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/AuthProviderTracker.java @@ -0,0 +1,91 @@ +/* + * 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.ssh.osgi; + +import com.google.common.base.Preconditions; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.opendaylight.controller.netconf.auth.AuthConstants; +import org.opendaylight.controller.netconf.auth.AuthProvider; +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; + +final class AuthProviderTracker implements ServiceTrackerCustomizer, PasswordAuthenticator { + private static final Logger logger = LoggerFactory.getLogger(AuthProviderTracker.class); + + private final BundleContext bundleContext; + + private Integer maxPreference; + private final ServiceTracker listenerTracker; + private AuthProvider authProvider; + + public AuthProviderTracker(final BundleContext bundleContext) { + this.bundleContext = bundleContext; + listenerTracker = new ServiceTracker<>(bundleContext, AuthProvider.class, this); + listenerTracker.open(); + } + + @Override + public AuthProvider addingService(final ServiceReference reference) { + logger.trace("Service {} added", reference); + final AuthProvider authService = bundleContext.getService(reference); + final Integer newServicePreference = getPreference(reference); + if(isBetter(newServicePreference)) { + maxPreference = newServicePreference; + this.authProvider = authService; + } + return authService; + } + + private Integer getPreference(final ServiceReference reference) { + final Object preferenceProperty = reference.getProperty(AuthConstants.SERVICE_PREFERENCE_KEY); + return preferenceProperty == null ? Integer.MIN_VALUE : Integer.valueOf(preferenceProperty.toString()); + } + + private boolean isBetter(final Integer newServicePreference) { + Preconditions.checkNotNull(newServicePreference); + if(maxPreference == null) { + return true; + } + + return newServicePreference > maxPreference; + } + + @Override + public void modifiedService(final ServiceReference reference, final AuthProvider service) { + final AuthProvider authService = bundleContext.getService(reference); + final Integer newServicePreference = getPreference(reference); + if(isBetter(newServicePreference)) { + logger.trace("Replacing modified service {} in netconf SSH.", reference); + this.authProvider = authService; + } + } + + @Override + public void removedService(final ServiceReference reference, final AuthProvider service) { + logger.trace("Removing service {} from netconf SSH. " + + "SSH won't authenticate users until AuthProvider service will be started.", reference); + maxPreference = null; + this.authProvider = null; + } + + public void stop() { + listenerTracker.close(); + // sshThread should finish normally since sshServer.close stops processing + } + + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return authProvider == null ? false : authProvider.authenticated(username, password); + } +} diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java index 0d0f95c3cb..b871d19db8 100644 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java +++ b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/osgi/NetconfSSHActivator.java @@ -9,51 +9,51 @@ package org.opendaylight.controller.netconf.ssh.osgi; import static com.google.common.base.Preconditions.checkState; -import com.google.common.base.Preconditions; -import java.io.File; +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.nio.NioEventLoopGroup; import java.io.IOException; import java.net.InetSocketAddress; - +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; import org.apache.commons.io.FilenameUtils; -import org.opendaylight.controller.netconf.auth.AuthConstants; -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.apache.sshd.common.util.ThreadUtils; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; 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.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; -import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; -import com.google.common.base.Strings; - -import io.netty.channel.EventLoopGroup; -import io.netty.channel.local.LocalAddress; -import io.netty.channel.nio.NioEventLoopGroup; - -/** - * 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.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 { private static final Logger logger = LoggerFactory.getLogger(NetconfSSHActivator.class); - private static AuthProviderTracker authProviderTracker; - private NetconfSSHServer server; + private static final java.lang.String ALGORITHM = "RSA"; + private static final int KEY_SIZE = 4096; + public static final int POOL_SIZE = 8; + + private ScheduledExecutorService minaTimerExecutor; + private NioEventLoopGroup clientGroup; + private ExecutorService nioExecutor; + private AuthProviderTracker authProviderTracker; + + private SshProxyServer server; @Override public void start(final BundleContext bundleContext) throws IOException { + minaTimerExecutor = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + return new Thread(r, "netconf-ssh-server-mina-timers"); + } + }); + clientGroup = new NioEventLoopGroup(); + nioExecutor = ThreadUtils.newFixedThreadPool("netconf-ssh-server-nio-group", POOL_SIZE); server = startSSHServer(bundleContext); } @@ -66,11 +66,22 @@ public class NetconfSSHActivator implements BundleActivator { if(authProviderTracker != null) { authProviderTracker.stop(); } + + if(nioExecutor!=null) { + nioExecutor.shutdownNow(); + } + + if(clientGroup != null) { + clientGroup.shutdownGracefully(); + } + + if(minaTimerExecutor != null) { + minaTimerExecutor.shutdownNow(); + } } - private static NetconfSSHServer startSSHServer(final BundleContext bundleContext) throws IOException { - final Optional maybeSshSocketAddress = NetconfConfigUtil.extractNetconfServerAddress(bundleContext, - InfixProp.ssh); + private SshProxyServer startSSHServer(final BundleContext bundleContext) throws IOException { + final Optional maybeSshSocketAddress = NetconfConfigUtil.extractNetconfServerAddress(bundleContext, InfixProp.ssh); if (maybeSshSocketAddress.isPresent() == false) { logger.trace("SSH bridge not configured"); @@ -82,92 +93,15 @@ public class NetconfSSHActivator implements BundleActivator { final LocalAddress localAddress = NetconfConfigUtil.getNetconfLocalAddress(); - final String path = FilenameUtils.separatorsToSystem(NetconfConfigUtil.getPrivateKeyPath(bundleContext)); - checkState(!Strings.isNullOrEmpty(path), "Path to ssh private key is blank. Reconfigure %s", NetconfConfigUtil.getPrivateKeyKey()); - final String privateKeyPEMString = PEMGenerator.readOrGeneratePK(new File(path)); - - final EventLoopGroup bossGroup = new NioEventLoopGroup(); - final NetconfSSHServer server = NetconfSSHServer.start(sshSocketAddress.getPort(), localAddress, bossGroup, privateKeyPEMString.toCharArray()); - - authProviderTracker = new AuthProviderTracker(bundleContext, server); + authProviderTracker = new AuthProviderTracker(bundleContext); - return server; - } + final String path = FilenameUtils.separatorsToSystem(NetconfConfigUtil.getPrivateKeyPath(bundleContext)); + checkState(!Strings.isNullOrEmpty(path), "Path to ssh private key is blank. Reconfigure %s", + NetconfConfigUtil.getPrivateKeyKey()); - private static Thread runNetconfSshThread(final NetconfSSHServer server) { - final Thread serverThread = new Thread(server, "netconf SSH server thread"); - serverThread.setDaemon(true); - serverThread.start(); - logger.trace("Netconf SSH bridge up and running."); - return serverThread; + final SshProxyServer sshProxyServer = new SshProxyServer(minaTimerExecutor, clientGroup, nioExecutor); + sshProxyServer.bind(sshSocketAddress, localAddress, authProviderTracker, new PEMGeneratorHostKeyProvider(path, ALGORITHM, KEY_SIZE)); + return sshProxyServer; } - private static class AuthProviderTracker implements ServiceTrackerCustomizer { - private final BundleContext bundleContext; - private final NetconfSSHServer server; - - private Integer maxPreference; - private Thread sshThread; - private final ServiceTracker listenerTracker; - - public AuthProviderTracker(final BundleContext bundleContext, final NetconfSSHServer server) { - this.bundleContext = bundleContext; - this.server = server; - listenerTracker = new ServiceTracker<>(bundleContext, AuthProvider.class, this); - listenerTracker.open(); - } - - @Override - public AuthProvider addingService(final ServiceReference reference) { - logger.trace("Service {} added", reference); - final AuthProvider authService = bundleContext.getService(reference); - final Integer newServicePreference = getPreference(reference); - if(isBetter(newServicePreference)) { - maxPreference = newServicePreference; - server.setAuthProvider(authService); - if(sshThread == null) { - sshThread = runNetconfSshThread(server); - } - } - return authService; - } - - private Integer getPreference(final ServiceReference reference) { - final Object preferenceProperty = reference.getProperty(AuthConstants.SERVICE_PREFERENCE_KEY); - return preferenceProperty == null ? Integer.MIN_VALUE : Integer.valueOf(preferenceProperty.toString()); - } - - private boolean isBetter(final Integer newServicePreference) { - Preconditions.checkNotNull(newServicePreference); - if(maxPreference == null) { - return true; - } - - return newServicePreference > maxPreference; - } - - @Override - public void modifiedService(final ServiceReference reference, final AuthProvider service) { - final AuthProvider authService = bundleContext.getService(reference); - final Integer newServicePreference = getPreference(reference); - if(isBetter(newServicePreference)) { - logger.trace("Replacing modified service {} in netconf SSH.", reference); - server.setAuthProvider(authService); - } - } - - @Override - public void removedService(final ServiceReference reference, final AuthProvider service) { - logger.trace("Removing service {} from netconf SSH. " + - "SSH won't authenticate users until AuthProvider service will be started.", reference); - maxPreference = null; - server.setAuthProvider(null); - } - - public void stop() { - listenerTracker.close(); - // sshThread should finish normally since sshServer.close stops processing - } - - } } diff --git a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java b/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java deleted file mode 100644 index eec6c3a097..0000000000 --- a/opendaylight/netconf/netconf-ssh/src/main/java/org/opendaylight/controller/netconf/ssh/threads/Handshaker.java +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.controller.netconf.ssh.threads; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; - -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.util.messages.NetconfHelloMessageAdditionalHeader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.ethz.ssh2.AuthenticationResult; -import ch.ethz.ssh2.PtySettings; -import ch.ethz.ssh2.ServerAuthenticationCallback; -import ch.ethz.ssh2.ServerConnection; -import ch.ethz.ssh2.ServerConnectionCallback; -import ch.ethz.ssh2.ServerSession; -import ch.ethz.ssh2.ServerSessionCallback; -import ch.ethz.ssh2.SimpleServerSessionCallback; - -import com.google.common.base.Supplier; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufProcessor; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.local.LocalAddress; -import io.netty.channel.local.LocalChannel; -import io.netty.handler.stream.ChunkedStream; - -/** - * One instance represents per connection, responsible for ssh handshake. - * Once auth succeeds and correct subsystem is chosen, backend connection with - * netty netconf server is made. This task finishes right after negotiation is done. - */ -@ThreadSafe -public class Handshaker implements Runnable { - private static final Logger logger = LoggerFactory.getLogger(Handshaker.class); - - private final ServerConnection ganymedConnection; - private final String session; - - - public Handshaker(Socket socket, LocalAddress localAddress, long sessionId, AuthProvider authProvider, - EventLoopGroup bossGroup, final char[] pem) throws IOException { - - this.session = "Session " + sessionId; - - String remoteAddressWithPort = socket.getRemoteSocketAddress().toString().replace("/", ""); - logger.debug("{} started with {}", session, remoteAddressWithPort); - String remoteAddress, remotePort; - if (remoteAddressWithPort.contains(":")) { - String[] split = remoteAddressWithPort.split(":"); - remoteAddress = split[0]; - remotePort = split[1]; - } else { - remoteAddress = remoteAddressWithPort; - remotePort = ""; - } - ServerAuthenticationCallbackImpl serverAuthenticationCallback = new ServerAuthenticationCallbackImpl( - authProvider, session); - - ganymedConnection = new ServerConnection(socket); - - ServerConnectionCallbackImpl serverConnectionCallback = new ServerConnectionCallbackImpl( - serverAuthenticationCallback, remoteAddress, remotePort, session, - getGanymedAutoCloseable(ganymedConnection), localAddress, bossGroup); - - // initialize ganymed - ganymedConnection.setPEMHostKey(pem, null); - ganymedConnection.setAuthenticationCallback(serverAuthenticationCallback); - ganymedConnection.setServerConnectionCallback(serverConnectionCallback); - } - - - private static AutoCloseable getGanymedAutoCloseable(final ServerConnection ganymedConnection) { - return new AutoCloseable() { - @Override - public void close() throws Exception { - ganymedConnection.close(); - } - }; - } - - @Override - public void run() { - // let ganymed process handshake - logger.trace("{} is started", session); - try { - // TODO this should be guarded with a timer to prevent resource exhaustion - ganymedConnection.connect(); - } catch (IOException e) { - logger.debug("{} connection error", session, e); - } - logger.trace("{} is exiting", session); - } -} - -/** - * Netty client handler that forwards bytes from backed server to supplied output stream. - * When backend server closes the connection, remoteConnection.close() is called to tear - * down ssh connection. - */ -class SSHClientHandler extends ChannelInboundHandlerAdapter { - private static final Logger logger = LoggerFactory.getLogger(SSHClientHandler.class); - private final AutoCloseable remoteConnection; - private final BufferedOutputStream remoteOutputStream; - private final String session; - private ChannelHandlerContext channelHandlerContext; - - public SSHClientHandler(AutoCloseable remoteConnection, OutputStream remoteOutputStream, - String session) { - this.remoteConnection = remoteConnection; - this.remoteOutputStream = new BufferedOutputStream(remoteOutputStream); - this.session = session; - } - - @Override - public void channelActive(ChannelHandlerContext ctx) { - this.channelHandlerContext = ctx; - logger.debug("{} Client active", session); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws IOException { - ByteBuf bb = (ByteBuf) msg; - // we can block the server here so that slow client does not cause memory pressure - try { - bb.forEachByte(new ByteBufProcessor() { - @Override - public boolean process(byte value) throws Exception { - remoteOutputStream.write(value); - return true; - } - }); - } finally { - bb.release(); - } - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws IOException { - logger.trace("{} Flushing", session); - remoteOutputStream.flush(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - // Close the connection when an exception is raised. - logger.warn("{} Unexpected exception from downstream", session, cause); - ctx.close(); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - logger.trace("{} channelInactive() called, closing remote client ctx", session); - remoteConnection.close();//this should close socket and all threads created for this client - this.channelHandlerContext = null; - } - - public ChannelHandlerContext getChannelHandlerContext() { - return checkNotNull(channelHandlerContext, "Channel is not active"); - } -} - -/** - * Ganymed handler that gets unencrypted input and output streams, connects them to netty. - * Checks that 'netconf' subsystem is chosen by user. - * Launches new ClientInputStreamPoolingThread thread once session is established. - * Writes custom header to netty server, to inform it about IP address and username. - */ -class ServerConnectionCallbackImpl implements ServerConnectionCallback { - private static final Logger logger = LoggerFactory.getLogger(ServerConnectionCallbackImpl.class); - public static final String NETCONF_SUBSYSTEM = "netconf"; - - private final Supplier currentUserSupplier; - private final String remoteAddress; - private final String remotePort; - private final String session; - private final AutoCloseable ganymedConnection; - private final LocalAddress localAddress; - private final EventLoopGroup bossGroup; - - ServerConnectionCallbackImpl(Supplier currentUserSupplier, String remoteAddress, String remotePort, String session, - AutoCloseable ganymedConnection, LocalAddress localAddress, EventLoopGroup bossGroup) { - this.currentUserSupplier = currentUserSupplier; - this.remoteAddress = remoteAddress; - this.remotePort = remotePort; - this.session = session; - this.ganymedConnection = ganymedConnection; - // initialize netty local connection - this.localAddress = localAddress; - this.bossGroup = bossGroup; - } - - private static ChannelFuture initializeNettyConnection(LocalAddress localAddress, EventLoopGroup bossGroup, - final SSHClientHandler sshClientHandler) { - Bootstrap clientBootstrap = new Bootstrap(); - clientBootstrap.group(bossGroup).channel(LocalChannel.class); - - clientBootstrap.handler(new ChannelInitializer() { - @Override - public void initChannel(LocalChannel ch) throws Exception { - ch.pipeline().addLast(sshClientHandler); - } - }); - // asynchronously initialize local connection to netconf server - return clientBootstrap.connect(localAddress); - } - - @Override - public ServerSessionCallback acceptSession(final ServerSession serverSession) { - String currentUser = currentUserSupplier.get(); - final String additionalHeader = new NetconfHelloMessageAdditionalHeader(currentUser, remoteAddress, - remotePort, "ssh", "client").toFormattedString(); - - - return new SimpleServerSessionCallback() { - @Override - public Runnable requestSubsystem(final ServerSession ss, final String subsystem) throws IOException { - return new Runnable() { - @Override - public void run() { - if (NETCONF_SUBSYSTEM.equals(subsystem)) { - // connect - final SSHClientHandler sshClientHandler = new SSHClientHandler(ganymedConnection, ss.getStdin(), session); - ChannelFuture clientChannelFuture = initializeNettyConnection(localAddress, bossGroup, sshClientHandler); - // get channel - final Channel channel = clientChannelFuture.awaitUninterruptibly().channel(); - - // write additional header before polling thread is started - // polling thread could process and forward data before additional header is written - // This will result into unexpected state: hello message without additional header and the next message with additional header - channel.writeAndFlush(Unpooled.copiedBuffer(additionalHeader.getBytes())); - - new ClientInputStreamPoolingThread(session, ss.getStdout(), channel, new AutoCloseable() { - @Override - public void close() throws Exception { - logger.trace("Closing both ganymed and local connection"); - try { - ganymedConnection.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing ganymed", e); - } - try { - channel.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing channel", e); - } - } - }, sshClientHandler.getChannelHandlerContext()).start(); - } else { - logger.debug("{} Wrong subsystem requested:'{}', closing ssh session", serverSession, subsystem); - String reason = "Only netconf subsystem is supported, requested:" + subsystem; - closeSession(ss, reason); - } - } - }; - } - - public void closeSession(ServerSession ss, String reason) { - logger.trace("{} Closing session - {}", serverSession, reason); - try { - ss.getStdin().write(reason.getBytes()); - } catch (IOException e) { - logger.warn("{} Exception while closing session", serverSession, e); - } - ss.close(); - } - - @Override - public Runnable requestPtyReq(final ServerSession ss, final PtySettings pty) throws IOException { - return new Runnable() { - @Override - public void run() { - closeSession(ss, "PTY request not supported"); - } - }; - } - - @Override - public Runnable requestShell(final ServerSession ss) throws IOException { - return new Runnable() { - @Override - public void run() { - closeSession(ss, "Shell not supported"); - } - }; - } - }; - } -} - -/** - * Only thread that is required during ssh session, forwards client's input to netty. - * When user closes connection, onEndOfInput.close() is called to tear down the local channel. - */ -class ClientInputStreamPoolingThread extends Thread { - private static final Logger logger = LoggerFactory.getLogger(ClientInputStreamPoolingThread.class); - - private final InputStream fromClientIS; - private final Channel serverChannel; - private final AutoCloseable onEndOfInput; - private final ChannelHandlerContext channelHandlerContext; - - ClientInputStreamPoolingThread(String session, InputStream fromClientIS, Channel serverChannel, AutoCloseable onEndOfInput, - ChannelHandlerContext channelHandlerContext) { - super(ClientInputStreamPoolingThread.class.getSimpleName() + " " + session); - this.fromClientIS = fromClientIS; - this.serverChannel = serverChannel; - this.onEndOfInput = onEndOfInput; - this.channelHandlerContext = channelHandlerContext; - } - - @Override - public void run() { - ChunkedStream chunkedStream = new ChunkedStream(fromClientIS); - try { - ByteBuf byteBuf; - while ((byteBuf = chunkedStream.readChunk(channelHandlerContext/*only needed for ByteBuf alloc */)) != null) { - serverChannel.writeAndFlush(byteBuf); - } - } catch (Exception e) { - logger.warn("Exception", e); - } finally { - logger.trace("End of input"); - // tear down connection - try { - onEndOfInput.close(); - } catch (Exception e) { - logger.warn("Ignoring exception while closing socket", e); - } - } - } -} - -/** - * Authentication handler for ganymed. - * Provides current user name after authenticating using supplied AuthProvider. - */ -@NotThreadSafe -class ServerAuthenticationCallbackImpl implements ServerAuthenticationCallback, Supplier { - private static final Logger logger = LoggerFactory.getLogger(ServerAuthenticationCallbackImpl.class); - private final AuthProvider authProvider; - private final String session; - private String currentUser; - - ServerAuthenticationCallbackImpl(AuthProvider authProvider, String session) { - this.authProvider = authProvider; - this.session = session; - } - - @Override - public String initAuthentication(ServerConnection sc) { - logger.trace("{} Established connection", session); - return "Established connection" + "\r\n"; - } - - @Override - public String[] getRemainingAuthMethods(ServerConnection sc) { - return new String[]{ServerAuthenticationCallback.METHOD_PASSWORD}; - } - - @Override - public AuthenticationResult authenticateWithNone(ServerConnection sc, String username) { - return AuthenticationResult.FAILURE; - } - - @Override - public AuthenticationResult authenticateWithPassword(ServerConnection sc, String username, String password) { - checkState(currentUser == null); - try { - if (authProvider.authenticated(username, password)) { - currentUser = username; - logger.trace("{} user {} authenticated", session, currentUser); - return AuthenticationResult.SUCCESS; - } - } catch (Exception e) { - logger.warn("{} Authentication failed", session, e); - } - return AuthenticationResult.FAILURE; - } - - @Override - public AuthenticationResult authenticateWithPublicKey(ServerConnection sc, String username, String algorithm, - byte[] publicKey, byte[] signature) { - return AuthenticationResult.FAILURE; - } - - @Override - public String get() { - return currentUser; - } -} diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java index eb2b644cbc..62ce587237 100644 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/netty/SSHTest.java @@ -11,9 +11,6 @@ package org.opendaylight.controller.netconf.netty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import com.google.common.base.Stopwatch; import io.netty.bootstrap.Bootstrap; @@ -23,16 +20,21 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.HashedWheelTimer; import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; -import org.opendaylight.controller.netconf.auth.AuthProvider; import org.opendaylight.controller.netconf.netty.EchoClientHandler.State; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.authentication.LoginPassword; import org.opendaylight.controller.netconf.nettyutil.handler.ssh.client.AsyncSshHandler; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,68 +42,77 @@ import org.slf4j.LoggerFactory; public class SSHTest { public static final Logger logger = LoggerFactory.getLogger(SSHTest.class); public static final String AHOJ = "ahoj\n"; - private EventLoopGroup nettyGroup; - HashedWheelTimer hashedWheelTimer; - @Before - public void setUp() throws Exception { + private static EventLoopGroup nettyGroup; + private static HashedWheelTimer hashedWheelTimer; + private static ExecutorService nioExec; + private static ScheduledExecutorService minaTimerEx; + + @BeforeClass + public static void setUp() throws Exception { hashedWheelTimer = new HashedWheelTimer(); nettyGroup = new NioEventLoopGroup(); + nioExec = Executors.newFixedThreadPool(1); + minaTimerEx = Executors.newScheduledThreadPool(1); } - @After - public void tearDown() throws Exception { + @AfterClass + public static void tearDown() throws Exception { hashedWheelTimer.stop(); - nettyGroup.shutdownGracefully(); + nettyGroup.shutdownGracefully().await(); + minaTimerEx.shutdownNow(); + nioExec.shutdownNow(); } @Test public void test() throws Exception { new Thread(new EchoServer(), "EchoServer").start(); - AuthProvider authProvider = mock(AuthProvider.class); - doReturn(true).when(authProvider).authenticated(anyString(), anyString()); - doReturn("auth").when(authProvider).toString(); - - NetconfSSHServer netconfSSHServer = NetconfSSHServer.start(10831, NetconfConfigUtil.getNetconfLocalAddress(), - new NioEventLoopGroup(), PEMGenerator.generate().toCharArray()); - netconfSSHServer.setAuthProvider(authProvider); - InetSocketAddress address = netconfSSHServer.getLocalSocketAddress(); + final InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 10831); + final SshProxyServer sshProxyServer = new SshProxyServer(minaTimerEx, nettyGroup, nioExec); + sshProxyServer.bind(addr, NetconfConfigUtil.getNetconfLocalAddress(), + new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return true; + } + }, new PEMGeneratorHostKeyProvider(Files.createTempFile("prefix", "suffix").toAbsolutePath().toString())); - final EchoClientHandler echoClientHandler = connectClient(new InetSocketAddress("localhost", address.getPort())); + final EchoClientHandler echoClientHandler = connectClient(addr); Stopwatch stopwatch = new Stopwatch().start(); - while(echoClientHandler.isConnected() == false && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(100); + while(echoClientHandler.isConnected() == false && stopwatch.elapsed(TimeUnit.SECONDS) < 30) { + Thread.sleep(500); } assertTrue(echoClientHandler.isConnected()); logger.info("connected, writing to client"); echoClientHandler.write(AHOJ); + // check that server sent back the same string stopwatch = stopwatch.reset().start(); - while (echoClientHandler.read().endsWith(AHOJ) == false && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(100); + while (echoClientHandler.read().endsWith(AHOJ) == false && stopwatch.elapsed(TimeUnit.SECONDS) < 30) { + Thread.sleep(500); } + try { - String read = echoClientHandler.read(); + final String read = echoClientHandler.read(); assertTrue(read + " should end with " + AHOJ, read.endsWith(AHOJ)); } finally { logger.info("Closing socket"); - netconfSSHServer.close(); - netconfSSHServer.join(); + sshProxyServer.close(); } } - public EchoClientHandler connectClient(InetSocketAddress address) { + public EchoClientHandler connectClient(final InetSocketAddress address) { final EchoClientHandler echoClientHandler = new EchoClientHandler(); - ChannelInitializer channelInitializer = new ChannelInitializer() { + final ChannelInitializer channelInitializer = new ChannelInitializer() { @Override - public void initChannel(NioSocketChannel ch) throws Exception { + public void initChannel(final NioSocketChannel ch) throws Exception { ch.pipeline().addFirst(AsyncSshHandler.createForNetconfSubsystem(new LoginPassword("a", "a"))); ch.pipeline().addLast(echoClientHandler); } }; - Bootstrap b = new Bootstrap(); + final Bootstrap b = new Bootstrap(); b.group(nettyGroup) .channel(NioSocketChannel.class) @@ -114,9 +125,9 @@ public class SSHTest { @Test public void testClientWithoutServer() throws Exception { - InetSocketAddress address = new InetSocketAddress(12345); + final InetSocketAddress address = new InetSocketAddress(12345); final EchoClientHandler echoClientHandler = connectClient(address); - Stopwatch stopwatch = new Stopwatch().start(); + final Stopwatch stopwatch = new Stopwatch().start(); while(echoClientHandler.getState() == State.CONNECTING && stopwatch.elapsed(TimeUnit.SECONDS) < 5) { Thread.sleep(100); } diff --git a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java index 1151abcdf2..9cd0c9bcea 100644 --- a/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java +++ b/opendaylight/netconf/netconf-ssh/src/test/java/org/opendaylight/controller/netconf/ssh/authentication/SSHServerTest.java @@ -12,19 +12,26 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import ch.ethz.ssh2.Connection; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import java.io.InputStream; import java.net.InetSocketAddress; -import junit.framework.Assert; -import org.apache.commons.io.IOUtils; +import java.nio.file.Files; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.opendaylight.controller.netconf.auth.AuthProvider; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceListener; @@ -39,13 +46,15 @@ public class SSHServerTest { private static final String PASSWORD = "netconf"; private static final String HOST = "127.0.0.1"; private static final int PORT = 1830; - private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 8383); private static final Logger logger = LoggerFactory.getLogger(SSHServerTest.class); - private Thread sshServerThread; + + private SshProxyServer server; @Mock private BundleContext mockedContext; - + private final ExecutorService nioExec = Executors.newFixedThreadPool(1); + private final EventLoopGroup clientGroup = new NioEventLoopGroup(); + private final ScheduledExecutorService minaTimerEx = Executors.newScheduledThreadPool(1); @Before public void setUp() throws Exception { @@ -55,42 +64,39 @@ public class SSHServerTest { doReturn(new ServiceReference[0]).when(mockedContext).getServiceReferences(anyString(), anyString()); logger.info("Creating SSH server"); - String pem; - try (InputStream is = getClass().getResourceAsStream("/RSA.pk")) { - pem = IOUtils.toString(is); - } - - EventLoopGroup bossGroup = new NioEventLoopGroup(); - NetconfSSHServer server = NetconfSSHServer.start(PORT, NetconfConfigUtil.getNetconfLocalAddress(), - bossGroup, pem.toCharArray()); - server.setAuthProvider(new AuthProvider() { - @Override - public boolean authenticated(final String username, final String password) { - return true; - } - }); - - sshServerThread = new Thread(server); - sshServerThread.setDaemon(true); - sshServerThread.start(); - logger.info("SSH server on " + PORT); + final InetSocketAddress addr = InetSocketAddress.createUnresolved(HOST, PORT); + server = new SshProxyServer(minaTimerEx, clientGroup, nioExec); + server.bind(addr, NetconfConfigUtil.getNetconfLocalAddress(), + new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + return true; + } + }, new PEMGeneratorHostKeyProvider(Files.createTempFile("prefix", "suffix").toAbsolutePath().toString())); + logger.info("SSH server started on " + PORT); } @Test - public void connect() { + public void connect() throws Exception { + final SshClient sshClient = SshClient.setUpDefaultClient(); + sshClient.start(); try { - Connection conn = new Connection(HOST, PORT); - Assert.assertNotNull(conn); - logger.info("connecting to SSH server"); - conn.connect(); - logger.info("authenticating ..."); - boolean isAuthenticated = conn.authenticateWithPassword(USER, PASSWORD); - Assert.assertTrue(isAuthenticated); - } catch (Exception e) { - logger.error("Error while starting SSH server.", e); + final ConnectFuture connect = sshClient.connect(USER, HOST, PORT); + connect.await(30, TimeUnit.SECONDS); + org.junit.Assert.assertTrue(connect.isConnected()); + final ClientSession session = connect.getSession(); + session.addPasswordIdentity(PASSWORD); + final AuthFuture auth = session.auth(); + auth.await(30, TimeUnit.SECONDS); + org.junit.Assert.assertTrue(auth.isSuccess()); + } finally { + sshClient.close(true); + server.close(); + clientGroup.shutdownGracefully().await(); + minaTimerEx.shutdownNow(); + nioExec.shutdownNow(); } - } } diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java index d0939a288f..e8ba769da5 100644 --- a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java +++ b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java @@ -19,13 +19,13 @@ import com.google.common.collect.Sets; import com.google.common.io.CharStreams; import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.local.LocalAddress; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.HashedWheelTimer; import java.io.Closeable; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -34,6 +34,8 @@ import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.URI; import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractMap; import java.util.Date; import java.util.HashMap; @@ -42,8 +44,15 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.apache.sshd.common.util.ThreadUtils; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; import org.opendaylight.controller.netconf.api.monitoring.NetconfManagementSession; import org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants; import org.opendaylight.controller.netconf.impl.DefaultCommitNotificationProducer; @@ -58,8 +67,7 @@ import org.opendaylight.controller.netconf.mapping.api.NetconfOperationProvider; import org.opendaylight.controller.netconf.mapping.api.NetconfOperationService; import org.opendaylight.controller.netconf.mapping.api.NetconfOperationServiceSnapshot; import org.opendaylight.controller.netconf.monitoring.osgi.NetconfMonitoringOperationService; -import org.opendaylight.controller.netconf.ssh.NetconfSSHServer; -import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator; +import org.opendaylight.controller.netconf.ssh.SshProxyServer; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceException; import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation; @@ -85,14 +93,22 @@ public class NetconfDeviceSimulator implements Closeable { private final NioEventLoopGroup nettyThreadgroup; private final HashedWheelTimer hashedWheelTimer; private final List devicesChannels = Lists.newArrayList(); + private final List sshWrappers = Lists.newArrayList(); + private final ScheduledExecutorService minaTimerExecutor; + private final ExecutorService nioExecutor; public NetconfDeviceSimulator() { - this(new NioEventLoopGroup(), new HashedWheelTimer()); + // TODO make pool size configurable + this(new NioEventLoopGroup(), new HashedWheelTimer(), + Executors.newScheduledThreadPool(8, new ThreadFactoryBuilder().setNameFormat("netconf-ssh-server-mina-timers-%d").build()), + ThreadUtils.newFixedThreadPool("netconf-ssh-server-nio-group", 8)); } - public NetconfDeviceSimulator(final NioEventLoopGroup eventExecutors, final HashedWheelTimer hashedWheelTimer) { + private NetconfDeviceSimulator(final NioEventLoopGroup eventExecutors, final HashedWheelTimer hashedWheelTimer, final ScheduledExecutorService minaTimerExecutor, final ExecutorService nioExecutor) { this.nettyThreadgroup = eventExecutors; this.hashedWheelTimer = hashedWheelTimer; + this.minaTimerExecutor = minaTimerExecutor; + this.nioExecutor = nioExecutor; } private NetconfServerDispatcher createDispatcher(final Map moduleBuilders, final boolean exi, final int generateConfigsTimeout) { @@ -162,17 +178,31 @@ public class NetconfDeviceSimulator implements Closeable { int currentPort = params.startingPort; final List openDevices = Lists.newArrayList(); + + // Generate key to temp folder + final PEMGeneratorHostKeyProvider keyPairProvider = getPemGeneratorHostKeyProvider(); + for (int i = 0; i < params.deviceCount; i++) { final InetSocketAddress address = getAddress(currentPort); final ChannelFuture server; if(params.ssh) { + final InetSocketAddress bindingAddress = InetSocketAddress.createUnresolved("0.0.0.0", currentPort); final LocalAddress tcpLocalAddress = new LocalAddress(address.toString()); server = dispatcher.createLocalServer(tcpLocalAddress); try { - final NetconfSSHServer sshServer = NetconfSSHServer.start(currentPort, tcpLocalAddress, nettyThreadgroup, getPemArray()); - sshServer.setAuthProvider(new AcceptingAuthProvider()); + final SshProxyServer sshServer = new SshProxyServer(minaTimerExecutor, nettyThreadgroup, nioExecutor); + sshServer.bind(bindingAddress, tcpLocalAddress, + new PasswordAuthenticator() { + @Override + public boolean authenticate(final String username, final String password, final ServerSession session) { + // All connections are accepted + return true; + } + }, keyPairProvider); + + sshWrappers.add(sshServer); } catch (final Exception e) { LOG.warn("Cannot start simulated device on {}, skipping", address, e); // Close local server and continue @@ -225,10 +255,12 @@ public class NetconfDeviceSimulator implements Closeable { return openDevices; } - private char[] getPemArray() { + private PEMGeneratorHostKeyProvider getPemGeneratorHostKeyProvider() { try { - return PEMGenerator.readOrGeneratePK(new File("PK")).toCharArray(); + final Path tempFile = Files.createTempFile("tempKeyNetconfTest", "suffix"); + return new PEMGeneratorHostKeyProvider(tempFile.toAbsolutePath().toString()); } catch (final IOException e) { + LOG.error("Unable to generate PEM key", e); throw new RuntimeException(e); } } @@ -317,10 +349,15 @@ public class NetconfDeviceSimulator implements Closeable { @Override public void close() { + for (final SshProxyServer sshWrapper : sshWrappers) { + sshWrapper.close(); + } for (final Channel deviceCh : devicesChannels) { deviceCh.close(); } nettyThreadgroup.shutdownGracefully(); + minaTimerExecutor.shutdownNow(); + nioExecutor.shutdownNow(); // close Everything } diff --git a/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/xml/XmlElement.java b/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/xml/XmlElement.java index 78efe7e972..3c63204881 100644 --- a/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/xml/XmlElement.java +++ b/opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/xml/XmlElement.java @@ -37,6 +37,8 @@ import org.xml.sax.SAXException; public final class XmlElement { + public static final String DEFAULT_NAMESPACE_PREFIX = ""; + private final Element element; private static final Logger logger = LoggerFactory.getLogger(XmlElement.class); @@ -72,16 +74,16 @@ public final class XmlElement { return xmlElement; } - private static Map extractNamespaces(Element typeElement) throws NetconfDocumentedException { + private Map extractNamespaces() throws NetconfDocumentedException { Map namespaces = new HashMap<>(); - NamedNodeMap attributes = typeElement.getAttributes(); + NamedNodeMap attributes = element.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node attribute = attributes.item(i); String attribKey = attribute.getNodeName(); if (attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY)) { String prefix; if (attribKey.equals(XmlUtil.XMLNS_ATTRIBUTE_KEY)) { - prefix = ""; + prefix = DEFAULT_NAMESPACE_PREFIX; } else { if (!attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY + ":")){ throw new NetconfDocumentedException("Attribute doesn't start with :", @@ -94,6 +96,15 @@ public final class XmlElement { namespaces.put(prefix, attribute.getNodeValue()); } } + + // namespace does not have to be defined on this element but inherited + if(!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) { + Optional namespaceOptionally = getNamespaceOptionally(); + if(namespaceOptionally.isPresent()) { + namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get()); + } + } + return namespaces; } @@ -132,7 +143,7 @@ public final class XmlElement { } public String getName() { - if (element.getLocalName()!=null && !element.getLocalName().equals("")){ + if (element.getLocalName()!=null && !element.getLocalName().equals(DEFAULT_NAMESPACE_PREFIX)){ return element.getLocalName(); } return element.getTagName(); @@ -327,7 +338,7 @@ public final class XmlElement { public String getTextContent() throws NetconfDocumentedException { NodeList childNodes = element.getChildNodes(); if (childNodes.getLength() == 0) { - return ""; + return DEFAULT_NAMESPACE_PREFIX; } for(int i = 0; i < childNodes.getLength(); i++) { Node textChild = childNodes.item(i); @@ -356,7 +367,7 @@ public final class XmlElement { public String getNamespaceAttribute() throws MissingNameSpaceException { String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY); - if (attribute == null || attribute.equals("")){ + if (attribute == null || attribute.equals(DEFAULT_NAMESPACE_PREFIX)){ throw new MissingNameSpaceException(String.format("Element %s must specify namespace", toString()), NetconfDocumentedException.ErrorType.application, @@ -415,14 +426,14 @@ public final class XmlElement { * is found value will be null. */ public Map.Entry findNamespaceOfTextContent() throws NetconfDocumentedException { - Map namespaces = extractNamespaces(element); + Map namespaces = extractNamespaces(); String textContent = getTextContent(); int indexOfColon = textContent.indexOf(':'); String prefix; if (indexOfColon > -1) { prefix = textContent.substring(0, indexOfColon); } else { - prefix = ""; + prefix = DEFAULT_NAMESPACE_PREFIX; } if (!namespaces.containsKey(prefix)) { throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element) + ". Prefix from content is " diff --git a/opendaylight/netconf/pom.xml b/opendaylight/netconf/pom.xml index 361f4cfa08..b392c5b672 100644 --- a/opendaylight/netconf/pom.xml +++ b/opendaylight/netconf/pom.xml @@ -38,6 +38,8 @@ netconf-auth netconf-usermanager netconf-testtool + + netconf-artifacts diff --git a/pom.xml b/pom.xml index bb8ad1dbeb..53ef5601bb 100644 --- a/pom.xml +++ b/pom.xml @@ -53,8 +53,7 @@ - third-party/ganymed - + third-party/commons/thirdparty diff --git a/third-party/ganymed/pom.xml b/third-party/ganymed/pom.xml deleted file mode 100644 index 676e2a24f1..0000000000 --- a/third-party/ganymed/pom.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - 4.0.0 - - - org.opendaylight.controller - commons.thirdparty - 1.2.0-SNAPSHOT - ../commons/thirdparty - - - org.opendaylight.controller.thirdparty - ganymed - 1.2.0-SNAPSHOT - bundle - - - - org.osgi - org.osgi.core - 5.0.0 - - - ch.ethz.ganymed - ganymed-ssh2 - 261 - - - - - - - org.apache.felix - maven-bundle-plugin - true - - - ch.ethz.ssh2.* - ganymed-ssh2;scope=compile - true - - - - - org.apache.maven.plugins - maven-enforcer-plugin - ${enforcer.version} - - - enforce-no-snapshots - - enforce - - - - - - ch.ethz.ganymed:ganymed-ssh2:* - - - ch.ethz.ganymed:ganymed-ssh2:[261] - - - - - - - - - - - - diff --git a/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java b/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java deleted file mode 100644 index aa13c40d9d..0000000000 --- a/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java +++ /dev/null @@ -1,1424 +0,0 @@ -/* - * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. - * Please refer to the LICENSE.txt for licensing details. - */ - -package ch.ethz.ssh2; - -import java.io.CharArrayWriter; -import java.io.File; -import java.net.Socket; -import java.io.FileReader; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketTimeoutException; -import java.security.SecureRandom; -import java.util.List; -import java.util.Vector; - -import ch.ethz.ssh2.auth.AuthenticationManager; -import ch.ethz.ssh2.channel.ChannelManager; -import ch.ethz.ssh2.crypto.CryptoWishList; -import ch.ethz.ssh2.crypto.cipher.BlockCipherFactory; -import ch.ethz.ssh2.crypto.digest.MAC; -import ch.ethz.ssh2.packets.PacketIgnore; -import ch.ethz.ssh2.transport.KexManager; -import ch.ethz.ssh2.transport.TransportManager; -import ch.ethz.ssh2.util.TimeoutService; -import ch.ethz.ssh2.util.TimeoutService.TimeoutToken; - -/** - * A Connection is used to establish an encrypted TCP/IP - * connection to a SSH-2 server. - *

- * Typically, one - *

    - *
  1. creates a {@link #Connection(String) Connection} object.
  2. - *
  3. calls the {@link #connect() connect()} method.
  4. - *
  5. calls some of the authentication methods (e.g., {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()}).
  6. - *
  7. calls one or several times the {@link #openSession() openSession()} method.
  8. - *
  9. finally, one must close the connection and release resources with the {@link #close() close()} method.
  10. - *
- * - * @author Christian Plattner - * @version $Id: Connection.java 69 2013-08-09 06:39:56Z dkocher@sudo.ch $ - */ - -public class Connection -{ - /** - * The identifier presented to the SSH-2 server. This is the same - * as the "softwareversion" defined in RFC 4253. - *

- * NOTE: As per the RFC, the "softwareversion" string MUST consist of printable - * US-ASCII characters, with the exception of whitespace characters and the minus sign (-). - */ - private String softwareversion = String.format("Ganymed_%s", Version.getSpecification()); - - /* Will be used to generate all random data needed for the current connection. - * Note: SecureRandom.nextBytes() is thread safe. - */ - - private SecureRandom generator; - - private Socket precreatedSocket; - - public Connection(Socket socket) { - this.precreatedSocket = socket; - this.hostname = socket.getInetAddress().getHostName(); - this.port = socket.getPort(); - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @return The list of supported cipher algorithms by this implementation. - */ - public static synchronized String[] getAvailableCiphers() - { - return BlockCipherFactory.getDefaultCipherList(); - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @return The list of supported MAC algorthims by this implementation. - */ - public static synchronized String[] getAvailableMACs() - { - return MAC.getMacList(); - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @return The list of supported server host key algorthims by this implementation. - */ - public static synchronized String[] getAvailableServerHostKeyAlgorithms() - { - return KexManager.getDefaultServerHostkeyAlgorithmList(); - } - - private AuthenticationManager am; - - private boolean authenticated = false; - private ChannelManager cm; - - private CryptoWishList cryptoWishList = new CryptoWishList(); - - private DHGexParameters dhgexpara = new DHGexParameters(); - - private final String hostname; - - private final int port; - - private TransportManager tm; - - private boolean tcpNoDelay = false; - - private ProxyData proxyData = null; - - private List connectionMonitors = new Vector(); - - /** - * Prepares a fresh Connection object which can then be used - * to establish a connection to the specified SSH-2 server. - *

- * Same as {@link #Connection(String, int) Connection(hostname, 22)}. - * - * @param hostname the hostname of the SSH-2 server. - */ - public Connection(String hostname) - { - this(hostname, 22); - } - - /** - * Prepares a fresh Connection object which can then be used - * to establish a connection to the specified SSH-2 server. - * - * @param hostname - * the host where we later want to connect to. - * @param port - * port on the server, normally 22. - */ - public Connection(String hostname, int port) - { - this.hostname = hostname; - this.port = port; - } - - /** - * Prepares a fresh Connection object which can then be used - * to establish a connection to the specified SSH-2 server. - * - * @param hostname - * the host where we later want to connect to. - * @param port - * port on the server, normally 22. - * @param softwareversion - * Allows you to set a custom "softwareversion" string as defined in RFC 4253. - * NOTE: As per the RFC, the "softwareversion" string MUST consist of printable - * US-ASCII characters, with the exception of whitespace characters and the minus sign (-). - */ - public Connection(String hostname, int port, String softwareversion) - { - this.hostname = hostname; - this.port = port; - this.softwareversion = softwareversion; - } - - /** - * After a successful connect, one has to authenticate oneself. This method - * is based on DSA (it uses DSA to sign a challenge sent by the server). - *

- * If the authentication phase is complete, true will be - * returned. If the server does not accept the request (or if further - * authentication steps are needed), false is returned and - * one can retry either by using this or any other authentication method - * (use the getRemainingAuthMethods method to get a list of - * the remaining possible methods). - * - * @param user - * A String holding the username. - * @param pem - * A String containing the DSA private key of the - * user in OpenSSH key format (PEM, you can't miss the - * "-----BEGIN DSA PRIVATE KEY-----" tag). The string may contain - * linefeeds. - * @param password - * If the PEM string is 3DES encrypted ("DES-EDE3-CBC"), then you - * must specify the password. Otherwise, this argument will be - * ignored and can be set to null. - * - * @return whether the connection is now authenticated. - * @throws IOException - * - * @deprecated You should use one of the {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()} - * methods, this method is just a wrapper for it and will - * disappear in future builds. - * - */ - public synchronized boolean authenticateWithDSA(String user, String pem, String password) throws IOException - { - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - if (user == null) - throw new IllegalArgumentException("user argument is null"); - - if (pem == null) - throw new IllegalArgumentException("pem argument is null"); - - authenticated = am.authenticatePublicKey(user, pem.toCharArray(), password, getOrCreateSecureRND()); - - return authenticated; - } - - /** - * A wrapper that calls {@link #authenticateWithKeyboardInteractive(String, String[], InteractiveCallback) - * authenticateWithKeyboardInteractivewith} a null submethod list. - * - * @param user - * A String holding the username. - * @param cb - * An InteractiveCallback which will be used to - * determine the responses to the questions asked by the server. - * @return whether the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithKeyboardInteractive(String user, InteractiveCallback cb) - throws IOException - { - return authenticateWithKeyboardInteractive(user, null, cb); - } - - /** - * After a successful connect, one has to authenticate oneself. This method - * is based on "keyboard-interactive", specified in - * draft-ietf-secsh-auth-kbdinteract-XX. Basically, you have to define a - * callback object which will be feeded with challenges generated by the - * server. Answers are then sent back to the server. It is possible that the - * callback will be called several times during the invocation of this - * method (e.g., if the server replies to the callback's answer(s) with - * another challenge...) - *

- * If the authentication phase is complete, true will be - * returned. If the server does not accept the request (or if further - * authentication steps are needed), false is returned and - * one can retry either by using this or any other authentication method - * (use the getRemainingAuthMethods method to get a list of - * the remaining possible methods). - *

- * Note: some SSH servers advertise "keyboard-interactive", however, any - * interactive request will be denied (without having sent any challenge to - * the client). - * - * @param user - * A String holding the username. - * @param submethods - * An array of submethod names, see - * draft-ietf-secsh-auth-kbdinteract-XX. May be null - * to indicate an empty list. - * @param cb - * An InteractiveCallback which will be used to - * determine the responses to the questions asked by the server. - * - * @return whether the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithKeyboardInteractive(String user, String[] submethods, - InteractiveCallback cb) throws IOException - { - if (cb == null) - throw new IllegalArgumentException("Callback may not ne NULL!"); - - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - if (user == null) - throw new IllegalArgumentException("user argument is null"); - - authenticated = am.authenticateInteractive(user, submethods, cb); - - return authenticated; - } - - /** - * After a successful connect, one has to authenticate oneself. This method - * sends username and password to the server. - *

- * If the authentication phase is complete, true will be - * returned. If the server does not accept the request (or if further - * authentication steps are needed), false is returned and - * one can retry either by using this or any other authentication method - * (use the getRemainingAuthMethods method to get a list of - * the remaining possible methods). - *

- * Note: if this method fails, then please double-check that it is actually - * offered by the server (use {@link #getRemainingAuthMethods(String) getRemainingAuthMethods()}. - *

- * Often, password authentication is disabled, but users are not aware of it. - * Many servers only offer "publickey" and "keyboard-interactive". However, - * even though "keyboard-interactive" *feels* like password authentication - * (e.g., when using the putty or openssh clients) it is *not* the same mechanism. - * - * @param user - * @param password - * @return if the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithPassword(String user, String password) throws IOException - { - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - if (user == null) - throw new IllegalArgumentException("user argument is null"); - - if (password == null) - throw new IllegalArgumentException("password argument is null"); - - authenticated = am.authenticatePassword(user, password); - - return authenticated; - } - - /** - * After a successful connect, one has to authenticate oneself. - * This method can be used to explicitly use the special "none" - * authentication method (where only a username has to be specified). - *

- * Note 1: The "none" method may always be tried by clients, however as by - * the specs, the server will not explicitly announce it. In other words, - * the "none" token will never show up in the list returned by - * {@link #getRemainingAuthMethods(String)}. - *

- * Note 2: no matter which one of the authenticateWithXXX() methods - * you call, the library will always issue exactly one initial "none" - * authentication request to retrieve the initially allowed list of - * authentication methods by the server. Please read RFC 4252 for the - * details. - *

- * If the authentication phase is complete, true will be - * returned. If further authentication steps are needed, false - * is returned and one can retry by any other authentication method - * (use the getRemainingAuthMethods method to get a list of - * the remaining possible methods). - * - * @param user - * @return if the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithNone(String user) throws IOException - { - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - if (user == null) - throw new IllegalArgumentException("user argument is null"); - - /* Trigger the sending of the PacketUserauthRequestNone packet */ - /* (if not already done) */ - - authenticated = am.authenticateNone(user); - - return authenticated; - } - - /** - * After a successful connect, one has to authenticate oneself. - * The authentication method "publickey" works by signing a challenge - * sent by the server. The signature is either DSA or RSA based - it - * just depends on the type of private key you specify, either a DSA - * or RSA private key in PEM format. And yes, this is may seem to be a - * little confusing, the method is called "publickey" in the SSH-2 protocol - * specification, however since we need to generate a signature, you - * actually have to supply a private key =). - *

- * The private key contained in the PEM file may also be encrypted ("Proc-Type: 4,ENCRYPTED"). - * The library supports DES-CBC and DES-EDE3-CBC encryption, as well - * as the more exotic PEM encrpytions AES-128-CBC, AES-192-CBC and AES-256-CBC. - *

- * If the authentication phase is complete, true will be - * returned. If the server does not accept the request (or if further - * authentication steps are needed), false is returned and - * one can retry either by using this or any other authentication method - * (use the getRemainingAuthMethods method to get a list of - * the remaining possible methods). - *

- * NOTE PUTTY USERS: Event though your key file may start with "-----BEGIN..." - * it is not in the expected format. You have to convert it to the OpenSSH - * key format by using the "puttygen" tool (can be downloaded from the Putty - * website). Simply load your key and then use the "Conversions/Export OpenSSH key" - * functionality to get a proper PEM file. - * - * @param user - * A String holding the username. - * @param pemPrivateKey - * A char[] containing a DSA or RSA private key of the - * user in OpenSSH key format (PEM, you can't miss the - * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE KEY-----" - * tag). The char array may contain linebreaks/linefeeds. - * @param password - * If the PEM structure is encrypted ("Proc-Type: 4,ENCRYPTED") then - * you must specify a password. Otherwise, this argument will be ignored - * and can be set to null. - * - * @return whether the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithPublicKey(String user, char[] pemPrivateKey, String password) - throws IOException - { - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - if (user == null) - throw new IllegalArgumentException("user argument is null"); - - if (pemPrivateKey == null) - throw new IllegalArgumentException("pemPrivateKey argument is null"); - - authenticated = am.authenticatePublicKey(user, pemPrivateKey, password, getOrCreateSecureRND()); - - return authenticated; - } - - /** - * A convenience wrapper function which reads in a private key (PEM format, either DSA or RSA) - * and then calls authenticateWithPublicKey(String, char[], String). - *

- * NOTE PUTTY USERS: Event though your key file may start with "-----BEGIN..." - * it is not in the expected format. You have to convert it to the OpenSSH - * key format by using the "puttygen" tool (can be downloaded from the Putty - * website). Simply load your key and then use the "Conversions/Export OpenSSH key" - * functionality to get a proper PEM file. - * - * @param user - * A String holding the username. - * @param pemFile - * A File object pointing to a file containing a DSA or RSA - * private key of the user in OpenSSH key format (PEM, you can't miss the - * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE KEY-----" - * tag). - * @param password - * If the PEM file is encrypted then you must specify the password. - * Otherwise, this argument will be ignored and can be set to null. - * - * @return whether the connection is now authenticated. - * @throws IOException - */ - public synchronized boolean authenticateWithPublicKey(String user, File pemFile, String password) - throws IOException - { - if (pemFile == null) - throw new IllegalArgumentException("pemFile argument is null"); - - char[] buff = new char[256]; - - CharArrayWriter cw = new CharArrayWriter(); - - FileReader fr = new FileReader(pemFile); - - while (true) - { - int len = fr.read(buff); - if (len < 0) - break; - cw.write(buff, 0, len); - } - - fr.close(); - - return authenticateWithPublicKey(user, cw.toCharArray(), password); - } - - /** - * Add a {@link ConnectionMonitor} to this connection. Can be invoked at any time, - * but it is best to add connection monitors before invoking - * connect() to avoid glitches (e.g., you add a connection monitor after - * a successful connect(), but the connection has died in the mean time. Then, - * your connection monitor won't be notified.) - *

- * You can add as many monitors as you like. If a monitor has already been added, then - * this method does nothing. - * - * @see ConnectionMonitor - * - * @param cmon An object implementing the {@link ConnectionMonitor} interface. - */ - public synchronized void addConnectionMonitor(ConnectionMonitor cmon) - { - if (cmon == null) - throw new IllegalArgumentException("cmon argument is null"); - - if (!connectionMonitors.contains(cmon)) - { - connectionMonitors.add(cmon); - - if (tm != null) - tm.setConnectionMonitors(connectionMonitors); - } - } - - /** - * Remove a {@link ConnectionMonitor} from this connection. - * - * @param cmon - * @return whether the monitor could be removed - */ - public synchronized boolean removeConnectionMonitor(ConnectionMonitor cmon) - { - if (cmon == null) - throw new IllegalArgumentException("cmon argument is null"); - - boolean existed = connectionMonitors.remove(cmon); - - if (tm != null) - tm.setConnectionMonitors(connectionMonitors); - - return existed; - } - - /** - * Close the connection to the SSH-2 server. All assigned sessions will be - * closed, too. Can be called at any time. Don't forget to call this once - * you don't need a connection anymore - otherwise the receiver thread may - * run forever. - */ - public synchronized void close() - { - Throwable t = new Throwable("Closed due to user request."); - close(t, false); - } - - public synchronized void close(Throwable t, boolean hard) - { - if (cm != null) - cm.closeAllChannels(); - - if (tm != null) - { - tm.close(t, hard == false); - tm = null; - } - am = null; - cm = null; - authenticated = false; - } - - /** - * Same as {@link #connect(ServerHostKeyVerifier, int, int) connect(null, 0, 0)}. - * - * @return see comments for the {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} method. - * @throws IOException - */ - public synchronized ConnectionInfo connect() throws IOException - { - return connect(null, 0, 0); - } - - /** - * Same as {@link #connect(ServerHostKeyVerifier, int, int) connect(verifier, 0, 0)}. - * - * @return see comments for the {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} method. - * @throws IOException - */ - public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier) throws IOException - { - return connect(verifier, 0, 0); - } - - /** - * Connect to the SSH-2 server and, as soon as the server has presented its - * host key, use the {@link ServerHostKeyVerifier#verifyServerHostKey(String, - * int, String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} - * method of the verifier to ask for permission to proceed. - * If verifier is null, then any host key will be - * accepted - this is NOT recommended, since it makes man-in-the-middle attackes - * VERY easy (somebody could put a proxy SSH server between you and the real server). - *

- * Note: The verifier will be called before doing any crypto calculations - * (i.e., diffie-hellman). Therefore, if you don't like the presented host key then - * no CPU cycles are wasted (and the evil server has less information about us). - *

- * However, it is still possible that the server presented a fake host key: the server - * cheated (typically a sign for a man-in-the-middle attack) and is not able to generate - * a signature that matches its host key. Don't worry, the library will detect such - * a scenario later when checking the signature (the signature cannot be checked before - * having completed the diffie-hellman exchange). - *

- * Note 2: The {@link ServerHostKeyVerifier#verifyServerHostKey(String, - * int, String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method - * will *NOT* be called from the current thread, the call is being made from a - * background thread (there is a background dispatcher thread for every - * established connection). - *

- * Note 3: This method will block as long as the key exchange of the underlying connection - * has not been completed (and you have not specified any timeouts). - *

- * Note 4: If you want to re-use a connection object that was successfully connected, - * then you must call the {@link #close()} method before invoking connect() again. - * - * @param verifier - * An object that implements the - * {@link ServerHostKeyVerifier} interface. Pass null - * to accept any server host key - NOT recommended. - * - * @param connectTimeout - * Connect the underlying TCP socket to the server with the given timeout - * value (non-negative, in milliseconds). Zero means no timeout. If a proxy is being - * used (see {@link #setProxyData(ProxyData)}), then this timeout is used for the - * connection establishment to the proxy. - * - * @param kexTimeout - * Timeout for complete connection establishment (non-negative, - * in milliseconds). Zero means no timeout. The timeout counts from the - * moment you invoke the connect() method and is cancelled as soon as the - * first key-exchange round has finished. It is possible that - * the timeout event will be fired during the invocation of the - * verifier callback, but it will only have an effect after - * the verifier returns. - * - * @return A {@link ConnectionInfo} object containing the details of - * the established connection. - * - * @throws IOException - * If any problem occurs, e.g., the server's host key is not - * accepted by the verifier or there is problem during - * the initial crypto setup (e.g., the signature sent by the server is wrong). - *

- * In case of a timeout (either connectTimeout or kexTimeout) - * a SocketTimeoutException is thrown. - *

- * An exception may also be thrown if the connection was already successfully - * connected (no matter if the connection broke in the mean time) and you invoke - * connect() again without having called {@link #close()} first. - *

- * If a HTTP proxy is being used and the proxy refuses the connection, - * then a {@link HTTPProxyException} may be thrown, which - * contains the details returned by the proxy. If the proxy is buggy and does - * not return a proper HTTP response, then a normal IOException is thrown instead. - */ - public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier, int connectTimeout, int kexTimeout) - throws IOException - { - final class TimeoutState - { - boolean isCancelled = false; - boolean timeoutSocketClosed = false; - } - - if (tm != null) - throw new IOException("Connection to " + hostname + " is already in connected state!"); - - if (connectTimeout < 0) - throw new IllegalArgumentException("connectTimeout must be non-negative!"); - - if (kexTimeout < 0) - throw new IllegalArgumentException("kexTimeout must be non-negative!"); - - final TimeoutState state = new TimeoutState(); - - tm = new TransportManager(); - tm.setSoTimeout(connectTimeout); - tm.setConnectionMonitors(connectionMonitors); - - /* Make sure that the runnable below will observe the new value of "tm" - * and "state" (the runnable will be executed in a different thread, which - * may be already running, that is why we need a memory barrier here). - * See also the comment in Channel.java if you - * are interested in the details. - * - * OKOK, this is paranoid since adding the runnable to the todo list - * of the TimeoutService will ensure that all writes have been flushed - * before the Runnable reads anything - * (there is a synchronized block in TimeoutService.addTimeoutHandler). - */ - - synchronized (tm) - { - /* We could actually synchronize on anything. */ - } - - try - { - TimeoutToken token = null; - - if (kexTimeout > 0) - { - final Runnable timeoutHandler = new Runnable() - { - public void run() - { - synchronized (state) - { - if (state.isCancelled) - return; - state.timeoutSocketClosed = true; - tm.close(new SocketTimeoutException("The connect timeout expired"), false); - } - } - }; - - long timeoutHorizont = System.currentTimeMillis() + kexTimeout; - - token = TimeoutService.addTimeoutHandler(timeoutHorizont, timeoutHandler); - } - - try - { - - if (precreatedSocket != null) { - tm.clientInit(precreatedSocket, softwareversion, cryptoWishList, verifier, dhgexpara, - getOrCreateSecureRND()); - } else { - tm.clientInit(hostname, port, softwareversion, cryptoWishList, verifier, dhgexpara, connectTimeout, - getOrCreateSecureRND(), proxyData); - } - } - catch (SocketTimeoutException se) - { - throw (SocketTimeoutException) new SocketTimeoutException( - "The connect() operation on the socket timed out.").initCause(se); - } - - tm.setTcpNoDelay(tcpNoDelay); - - /* Wait until first KEX has finished */ - - ConnectionInfo ci = tm.getConnectionInfo(1); - - /* Now try to cancel the timeout, if needed */ - - if (token != null) - { - TimeoutService.cancelTimeoutHandler(token); - - /* Were we too late? */ - - synchronized (state) - { - if (state.timeoutSocketClosed) - throw new IOException("This exception will be replaced by the one below =)"); - /* Just in case the "cancelTimeoutHandler" invocation came just a little bit - * too late but the handler did not enter the semaphore yet - we can - * still stop it. - */ - state.isCancelled = true; - } - } - - return ci; - } - catch (SocketTimeoutException ste) - { - throw ste; - } - catch (IOException e1) - { - /* This will also invoke any registered connection monitors */ - close(new Throwable("There was a problem during connect."), false); - - synchronized (state) - { - /* Show a clean exception, not something like "the socket is closed!?!" */ - if (state.timeoutSocketClosed) - throw new SocketTimeoutException("The kexTimeout (" + kexTimeout + " ms) expired."); - } - - /* Do not wrap a HTTPProxyException */ - if (e1 instanceof HTTPProxyException) - throw e1; - - throw (IOException) new IOException("There was a problem while connecting to " + hostname + ":" + port) - .initCause(e1); - } - } - - /** - * Creates a new {@link LocalPortForwarder}. - * A LocalPortForwarder forwards TCP/IP connections that arrive at a local - * port via the secure tunnel to another host (which may or may not be - * identical to the remote SSH-2 server). - *

- * This method must only be called after one has passed successfully the authentication step. - * There is no limit on the number of concurrent forwardings. - * - * @param local_port the local port the LocalPortForwarder shall bind to. - * @param host_to_connect target address (IP or hostname) - * @param port_to_connect target port - * @return A {@link LocalPortForwarder} object. - * @throws IOException - */ - public synchronized LocalPortForwarder createLocalPortForwarder(int local_port, String host_to_connect, - int port_to_connect) throws IOException - { - if (tm == null) - throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); - - return new LocalPortForwarder(cm, local_port, host_to_connect, port_to_connect); - } - - /** - * Creates a new {@link LocalPortForwarder}. - * A LocalPortForwarder forwards TCP/IP connections that arrive at a local - * port via the secure tunnel to another host (which may or may not be - * identical to the remote SSH-2 server). - *

- * This method must only be called after one has passed successfully the authentication step. - * There is no limit on the number of concurrent forwardings. - * - * @param addr specifies the InetSocketAddress where the local socket shall be bound to. - * @param host_to_connect target address (IP or hostname) - * @param port_to_connect target port - * @return A {@link LocalPortForwarder} object. - * @throws IOException - */ - public synchronized LocalPortForwarder createLocalPortForwarder(InetSocketAddress addr, String host_to_connect, - int port_to_connect) throws IOException - { - if (tm == null) - throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); - - return new LocalPortForwarder(cm, addr, host_to_connect, port_to_connect); - } - - /** - * Creates a new {@link LocalStreamForwarder}. - * A LocalStreamForwarder manages an Input/Outputstream pair - * that is being forwarded via the secure tunnel into a TCP/IP connection to another host - * (which may or may not be identical to the remote SSH-2 server). - * - * @param host_to_connect - * @param port_to_connect - * @return A {@link LocalStreamForwarder} object. - * @throws IOException - */ - public synchronized LocalStreamForwarder createLocalStreamForwarder(String host_to_connect, int port_to_connect) - throws IOException - { - if (tm == null) - throw new IllegalStateException("Cannot forward, you need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("Cannot forward, connection is not authenticated."); - - return new LocalStreamForwarder(cm, host_to_connect, port_to_connect); - } - - /** - * Create a very basic {@link SCPClient} that can be used to copy - * files from/to the SSH-2 server. - *

- * Works only after one has passed successfully the authentication step. - * There is no limit on the number of concurrent SCP clients. - *

- * Note: This factory method will probably disappear in the future. - * - * @return A {@link SCPClient} object. - * @throws IOException - */ - public synchronized SCPClient createSCPClient() throws IOException - { - if (tm == null) - throw new IllegalStateException("Cannot create SCP client, you need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("Cannot create SCP client, connection is not authenticated."); - - return new SCPClient(this); - } - - /** - * Force an asynchronous key re-exchange (the call does not block). The - * latest values set for MAC, Cipher and DH group exchange parameters will - * be used. If a key exchange is currently in progress, then this method has - * the only effect that the so far specified parameters will be used for the - * next (server driven) key exchange. - *

- * Note: This implementation will never start a key exchange (other than the initial one) - * unless you or the SSH-2 server ask for it. - * - * @throws IOException - * In case of any failure behind the scenes. - */ - public synchronized void forceKeyExchange() throws IOException - { - if (tm == null) - throw new IllegalStateException("You need to establish a connection first."); - - tm.forceKeyExchange(cryptoWishList, dhgexpara, null, null); - } - - /** - * Returns the hostname that was passed to the constructor. - * - * @return the hostname - */ - public synchronized String getHostname() - { - return hostname; - } - - /** - * Returns the port that was passed to the constructor. - * - * @return the TCP port - */ - public synchronized int getPort() - { - return port; - } - - /** - * Returns a {@link ConnectionInfo} object containing the details of - * the connection. Can be called as soon as the connection has been - * established (successfully connected). - * - * @return A {@link ConnectionInfo} object. - * @throws IOException - * In case of any failure behind the scenes. - */ - public synchronized ConnectionInfo getConnectionInfo() throws IOException - { - if (tm == null) - throw new IllegalStateException( - "Cannot get details of connection, you need to establish a connection first."); - return tm.getConnectionInfo(1); - } - - /** - * After a successful connect, one has to authenticate oneself. This method - * can be used to tell which authentication methods are supported by the - * server at a certain stage of the authentication process (for the given - * username). - *

- * Note 1: the username will only be used if no authentication step was done - * so far (it will be used to ask the server for a list of possible - * authentication methods by sending the initial "none" request). Otherwise, - * this method ignores the user name and returns a cached method list - * (which is based on the information contained in the last negative server response). - *

- * Note 2: the server may return method names that are not supported by this - * implementation. - *

- * After a successful authentication, this method must not be called - * anymore. - * - * @param user - * A String holding the username. - * - * @return a (possibly emtpy) array holding authentication method names. - * @throws IOException - */ - public synchronized String[] getRemainingAuthMethods(String user) throws IOException - { - if (user == null) - throw new IllegalArgumentException("user argument may not be NULL!"); - - if (tm == null) - throw new IllegalStateException("Connection is not established!"); - - if (authenticated) - throw new IllegalStateException("Connection is already authenticated!"); - - if (am == null) - am = new AuthenticationManager(tm); - - if (cm == null) - cm = new ChannelManager(tm); - - return am.getRemainingMethods(user); - } - - /** - * Determines if the authentication phase is complete. Can be called at any - * time. - * - * @return true if no further authentication steps are - * needed. - */ - public synchronized boolean isAuthenticationComplete() - { - return authenticated; - } - - /** - * Returns true if there was at least one failed authentication request and - * the last failed authentication request was marked with "partial success" - * by the server. This is only needed in the rare case of SSH-2 server setups - * that cannot be satisfied with a single successful authentication request - * (i.e., multiple authentication steps are needed.) - *

- * If you are interested in the details, then have a look at RFC4252. - * - * @return if the there was a failed authentication step and the last one - * was marked as a "partial success". - */ - public synchronized boolean isAuthenticationPartialSuccess() - { - if (am == null) - return false; - - return am.getPartialSuccess(); - } - - /** - * Checks if a specified authentication method is available. This method is - * actually just a wrapper for {@link #getRemainingAuthMethods(String) - * getRemainingAuthMethods()}. - * - * @param user - * A String holding the username. - * @param method - * An authentication method name (e.g., "publickey", "password", - * "keyboard-interactive") as specified by the SSH-2 standard. - * @return if the specified authentication method is currently available. - * @throws IOException - */ - public synchronized boolean isAuthMethodAvailable(String user, String method) throws IOException - { - if (method == null) - throw new IllegalArgumentException("method argument may not be NULL!"); - - String methods[] = getRemainingAuthMethods(user); - - for (int i = 0; i < methods.length; i++) - { - if (methods[i].compareTo(method) == 0) - return true; - } - - return false; - } - - private SecureRandom getOrCreateSecureRND() - { - if (generator == null) - generator = new SecureRandom(); - - return generator; - } - - /** - * Open a new {@link Session} on this connection. Works only after one has passed - * successfully the authentication step. There is no limit on the number of - * concurrent sessions. - * - * @return A {@link Session} object. - * @throws IOException - */ - public synchronized Session openSession() throws IOException - { - if (tm == null) - throw new IllegalStateException("Cannot open session, you need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("Cannot open session, connection is not authenticated."); - - return new Session(cm, getOrCreateSecureRND()); - } - - /** - * Send an SSH_MSG_IGNORE packet. This method will generate a random data attribute - * (length between 0 (invlusive) and 16 (exclusive) bytes, contents are random bytes). - *

- * This method must only be called once the connection is established. - * - * @throws IOException - */ - public synchronized void sendIgnorePacket() throws IOException - { - SecureRandom rnd = getOrCreateSecureRND(); - - byte[] data = new byte[rnd.nextInt(16)]; - rnd.nextBytes(data); - - sendIgnorePacket(data); - } - - /** - * Send an SSH_MSG_IGNORE packet with the given data attribute. - *

- * This method must only be called once the connection is established. - * - * @throws IOException - */ - public synchronized void sendIgnorePacket(byte[] data) throws IOException - { - if (data == null) - throw new IllegalArgumentException("data argument must not be null."); - - if (tm == null) - throw new IllegalStateException( - "Cannot send SSH_MSG_IGNORE packet, you need to establish a connection first."); - - PacketIgnore pi = new PacketIgnore(); - pi.setData(data); - - tm.sendMessage(pi.getPayload()); - } - - /** - * Removes duplicates from a String array, keeps only first occurence - * of each element. Does not destroy order of elements; can handle nulls. - * Uses a very efficient O(N^2) algorithm =) - * - * @param list a String array. - * @return a cleaned String array. - */ - private String[] removeDuplicates(String[] list) - { - if ((list == null) || (list.length < 2)) - return list; - - String[] list2 = new String[list.length]; - - int count = 0; - - for (int i = 0; i < list.length; i++) - { - boolean duplicate = false; - - String element = list[i]; - - for (int j = 0; j < count; j++) - { - if (((element == null) && (list2[j] == null)) || ((element != null) && (element.equals(list2[j])))) - { - duplicate = true; - break; - } - } - - if (duplicate) - continue; - - list2[count++] = list[i]; - } - - if (count == list2.length) - return list2; - - String[] tmp = new String[count]; - System.arraycopy(list2, 0, tmp, 0, count); - - return tmp; - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @param ciphers - */ - public synchronized void setClient2ServerCiphers(String[] ciphers) - { - if ((ciphers == null) || (ciphers.length == 0)) - throw new IllegalArgumentException(); - ciphers = removeDuplicates(ciphers); - BlockCipherFactory.checkCipherList(ciphers); - cryptoWishList.c2s_enc_algos = ciphers; - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @param macs - */ - public synchronized void setClient2ServerMACs(String[] macs) - { - if ((macs == null) || (macs.length == 0)) - throw new IllegalArgumentException(); - macs = removeDuplicates(macs); - MAC.checkMacList(macs); - cryptoWishList.c2s_mac_algos = macs; - } - - /** - * Sets the parameters for the diffie-hellman group exchange. Unless you - * know what you are doing, you will never need this. Default values are - * defined in the {@link DHGexParameters} class. - * - * @param dgp {@link DHGexParameters}, non null. - * - */ - public synchronized void setDHGexParameters(DHGexParameters dgp) - { - if (dgp == null) - throw new IllegalArgumentException(); - - dhgexpara = dgp; - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @param ciphers - */ - public synchronized void setServer2ClientCiphers(String[] ciphers) - { - if ((ciphers == null) || (ciphers.length == 0)) - throw new IllegalArgumentException(); - ciphers = removeDuplicates(ciphers); - BlockCipherFactory.checkCipherList(ciphers); - cryptoWishList.s2c_enc_algos = ciphers; - } - - /** - * Unless you know what you are doing, you will never need this. - * - * @param macs - */ - public synchronized void setServer2ClientMACs(String[] macs) - { - if ((macs == null) || (macs.length == 0)) - throw new IllegalArgumentException(); - - macs = removeDuplicates(macs); - MAC.checkMacList(macs); - cryptoWishList.s2c_mac_algos = macs; - } - - /** - * Define the set of allowed server host key algorithms to be used for - * the following key exchange operations. - *

- * Unless you know what you are doing, you will never need this. - * - * @param algos An array of allowed server host key algorithms. - * SSH-2 defines ssh-dss and ssh-rsa. - * The entries of the array must be ordered after preference, i.e., - * the entry at index 0 is the most preferred one. You must specify - * at least one entry. - */ - public synchronized void setServerHostKeyAlgorithms(String[] algos) - { - if ((algos == null) || (algos.length == 0)) - throw new IllegalArgumentException(); - - algos = removeDuplicates(algos); - KexManager.checkServerHostkeyAlgorithmsList(algos); - cryptoWishList.serverHostKeyAlgorithms = algos; - } - - /** - * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) on the underlying socket. - *

- * Can be called at any time. If the connection has not yet been established - * then the passed value will be stored and set after the socket has been set up. - * The default value that will be used is false. - * - * @param enable the argument passed to the Socket.setTCPNoDelay() method. - * @throws IOException - */ - public synchronized void setTCPNoDelay(boolean enable) throws IOException - { - tcpNoDelay = enable; - - if (tm != null) - tm.setTcpNoDelay(enable); - } - - /** - * Used to tell the library that the connection shall be established through a proxy server. - * It only makes sense to call this method before calling the {@link #connect() connect()} - * method. - *

- * At the moment, only HTTP proxies are supported. - *

- * Note: This method can be called any number of times. The {@link #connect() connect()} - * method will use the value set in the last preceding invocation of this method. - * - * @see HTTPProxyData - * - * @param proxyData Connection information about the proxy. If null, then - * no proxy will be used (non surprisingly, this is also the default). - */ - public synchronized void setProxyData(ProxyData proxyData) - { - this.proxyData = proxyData; - } - - /** - * Request a remote port forwarding. - * If successful, then forwarded connections will be redirected to the given target address. - * You can cancle a requested remote port forwarding by calling - * {@link #cancelRemotePortForwarding(int) cancelRemotePortForwarding()}. - *

- * A call of this method will block until the peer either agreed or disagreed to your request- - *

- * Note 1: this method typically fails if you - *

    - *
  • pass a port number for which the used remote user has not enough permissions (i.e., port - * < 1024)
  • - *
  • or pass a port number that is already in use on the remote server
  • - *
  • or if remote port forwarding is disabled on the server.
  • - *
- *

- * Note 2: (from the openssh man page): By default, the listening socket on the server will be - * bound to the loopback interface only. This may be overriden by specifying a bind address. - * Specifying a remote bind address will only succeed if the server's GatewayPorts option - * is enabled (see sshd_config(5)). - * - * @param bindAddress address to bind to on the server: - *

    - *
  • "" means that connections are to be accepted on all protocol families - * supported by the SSH implementation
  • - *
  • "0.0.0.0" means to listen on all IPv4 addresses
  • - *
  • "::" means to listen on all IPv6 addresses
  • - *
  • "localhost" means to listen on all protocol families supported by the SSH - * implementation on loopback addresses only, [RFC3330] and RFC3513]
  • - *
  • "127.0.0.1" and "::1" indicate listening on the loopback interfaces for - * IPv4 and IPv6 respectively
  • - *
- * @param bindPort port number to bind on the server (must be > 0) - * @param targetAddress the target address (IP or hostname) - * @param targetPort the target port - * @throws IOException - */ - public synchronized void requestRemotePortForwarding(String bindAddress, int bindPort, String targetAddress, - int targetPort) throws IOException - { - if (tm == null) - throw new IllegalStateException("You need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("The connection is not authenticated."); - - if ((bindAddress == null) || (targetAddress == null) || (bindPort <= 0) || (targetPort <= 0)) - throw new IllegalArgumentException(); - - cm.requestGlobalForward(bindAddress, bindPort, targetAddress, targetPort); - } - - /** - * Cancel an earlier requested remote port forwarding. - * Currently active forwardings will not be affected (e.g., disrupted). - * Note that further connection forwarding requests may be received until - * this method has returned. - * - * @param bindPort the allocated port number on the server - * @throws IOException if the remote side refuses the cancel request or another low - * level error occurs (e.g., the underlying connection is closed) - */ - public synchronized void cancelRemotePortForwarding(int bindPort) throws IOException - { - if (tm == null) - throw new IllegalStateException("You need to establish a connection first."); - - if (!authenticated) - throw new IllegalStateException("The connection is not authenticated."); - - cm.requestCancelGlobalForward(bindPort); - } - - /** - * Provide your own instance of SecureRandom. Can be used, e.g., if you - * want to seed the used SecureRandom generator manually. - *

- * The SecureRandom instance is used during key exchanges, public key authentication, - * x11 cookie generation and the like. - * - * @param rnd a SecureRandom instance - */ - public synchronized void setSecureRandom(SecureRandom rnd) - { - if (rnd == null) - throw new IllegalArgumentException(); - - this.generator = rnd; - } -} diff --git a/third-party/ganymed/src/main/java/ch/ethz/ssh2/channel/ChannelManager.java b/third-party/ganymed/src/main/java/ch/ethz/ssh2/channel/ChannelManager.java deleted file mode 100644 index 8fec9a0f54..0000000000 --- a/third-party/ganymed/src/main/java/ch/ethz/ssh2/channel/ChannelManager.java +++ /dev/null @@ -1,1845 +0,0 @@ -/* - * Copyright (c) 2006-2013 Christian Plattner. All rights reserved. - * Please refer to the LICENSE.txt for licensing details. - */ - -package ch.ethz.ssh2.channel; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Vector; - -import ch.ethz.ssh2.ChannelCondition; -import ch.ethz.ssh2.PtySettings; -import ch.ethz.ssh2.ServerConnectionCallback; -import ch.ethz.ssh2.ServerSessionCallback; -import ch.ethz.ssh2.log.Logger; -import ch.ethz.ssh2.packets.PacketChannelFailure; -import ch.ethz.ssh2.packets.PacketChannelOpenConfirmation; -import ch.ethz.ssh2.packets.PacketChannelOpenFailure; -import ch.ethz.ssh2.packets.PacketChannelSuccess; -import ch.ethz.ssh2.packets.PacketGlobalCancelForwardRequest; -import ch.ethz.ssh2.packets.PacketGlobalForwardRequest; -import ch.ethz.ssh2.packets.PacketOpenDirectTCPIPChannel; -import ch.ethz.ssh2.packets.PacketOpenSessionChannel; -import ch.ethz.ssh2.packets.PacketSessionExecCommand; -import ch.ethz.ssh2.packets.PacketSessionPtyRequest; -import ch.ethz.ssh2.packets.PacketSessionStartShell; -import ch.ethz.ssh2.packets.PacketSessionSubsystemRequest; -import ch.ethz.ssh2.packets.PacketSessionX11Request; -import ch.ethz.ssh2.packets.Packets; -import ch.ethz.ssh2.packets.TypesReader; -import ch.ethz.ssh2.server.ServerConnectionState; -import ch.ethz.ssh2.transport.MessageHandler; -import ch.ethz.ssh2.transport.TransportManager; - -/** - * ChannelManager. Please read the comments in Channel.java. - *

- * Besides the crypto part, this is the core of the library. - * - * @author Christian Plattner - * @version $Id: ChannelManager.java 48 2013-08-01 12:22:33Z cleondris@gmail.com $ - */ -public class ChannelManager implements MessageHandler -{ - private static final Logger log = Logger.getLogger(ChannelManager.class); - - private final ServerConnectionState server_state; - private final TransportManager tm; - - private final HashMap x11_magic_cookies = new HashMap(); - - private final List channels = new Vector(); - private int nextLocalChannel = 100; - private boolean shutdown = false; - private int globalSuccessCounter = 0; - private int globalFailedCounter = 0; - - private final HashMap remoteForwardings = new HashMap(); - - private final List listenerThreads = new Vector(); - - private boolean listenerThreadsAllowed = true; - - /** - * Constructor for client-mode. - * @param tm - */ - public ChannelManager(TransportManager tm) - { - this.server_state = null; - this.tm = tm; - tm.registerMessageHandler(this, 80, 100); - } - - /** - * Constructor for server-mode. - * @param state - */ - public ChannelManager(ServerConnectionState state) - { - this.server_state = state; - this.tm = state.tm; - tm.registerMessageHandler(this, 80, 100); - } - - private Channel getChannel(int id) - { - synchronized (channels) - { - for (Channel c : channels) - { - if (c.localID == id) - return c; - } - } - return null; - } - - private void removeChannel(int id) - { - synchronized (channels) - { - for (Channel c : channels) - { - if (c.localID == id) - { - channels.remove(c); - break; - } - } - } - } - - private int addChannel(Channel c) - { - synchronized (channels) - { - channels.add(c); - return nextLocalChannel++; - } - } - - private void waitUntilChannelOpen(Channel c) throws IOException - { - boolean wasInterrupted = false; - - synchronized (c) - { - while (c.state == Channel.STATE_OPENING) - { - try - { - c.wait(); - } - catch (InterruptedException ignore) - { - wasInterrupted = true; - } - } - - if (c.state != Channel.STATE_OPEN) - { - removeChannel(c.localID); - - String detail = c.getReasonClosed(); - - if (detail == null) - detail = "state: " + c.state; - - throw new IOException("Could not open channel (" + detail + ")"); - } - } - - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - - private void waitForGlobalSuccessOrFailure() throws IOException - { - boolean wasInterrupted = false; - - try - { - synchronized (channels) - { - while ((globalSuccessCounter == 0) && (globalFailedCounter == 0)) - { - if (shutdown) - { - throw new IOException("The connection is being shutdown"); - } - - try - { - channels.wait(); - } - catch (InterruptedException ignore) - { - wasInterrupted = true; - } - } - - if (globalFailedCounter != 0) - { - throw new IOException("The server denied the request (did you enable port forwarding?)"); - } - - if (globalSuccessCounter == 0) - { - throw new IOException("Illegal state."); - } - } - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - } - - private void waitForChannelSuccessOrFailure(Channel c) throws IOException - { - boolean wasInterrupted = false; - - try - { - synchronized (c) - { - while ((c.successCounter == 0) && (c.failedCounter == 0)) - { - if (c.state != Channel.STATE_OPEN) - { - String detail = c.getReasonClosed(); - - if (detail == null) - detail = "state: " + c.state; - - throw new IOException("This SSH2 channel is not open (" + detail + ")"); - } - - try - { - c.wait(); - } - catch (InterruptedException ignore) - { - wasInterrupted = true; - } - } - - if (c.failedCounter != 0) - { - throw new IOException("The server denied the request."); - } - } - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - } - - public void registerX11Cookie(String hexFakeCookie, X11ServerData data) - { - synchronized (x11_magic_cookies) - { - x11_magic_cookies.put(hexFakeCookie, data); - } - } - - public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels) - { - if (hexFakeCookie == null) - throw new IllegalStateException("hexFakeCookie may not be null"); - - synchronized (x11_magic_cookies) - { - x11_magic_cookies.remove(hexFakeCookie); - } - - if (killChannels == false) - return; - - log.debug("Closing all X11 channels for the given fake cookie"); - - List channel_copy = new Vector(); - - synchronized (channels) - { - channel_copy.addAll(channels); - } - - for (Channel c : channel_copy) - { - synchronized (c) - { - if (hexFakeCookie.equals(c.hexX11FakeCookie) == false) - continue; - } - - try - { - closeChannel(c, "Closing X11 channel since the corresponding session is closing", true); - } - catch (IOException ignored) - { - } - } - } - - public X11ServerData checkX11Cookie(String hexFakeCookie) - { - synchronized (x11_magic_cookies) - { - if (hexFakeCookie != null) - return x11_magic_cookies.get(hexFakeCookie); - } - return null; - } - - public void closeAllChannels() - { - log.debug("Closing all channels"); - - List channel_copy = new Vector(); - - synchronized (channels) - { - channel_copy.addAll(channels); - } - - for (Channel c : channel_copy) - { - try - { - closeChannel(c, "Closing all channels", true); - } - catch (IOException ignored) - { - } - } - } - - public void closeChannel(Channel c, String reason, boolean force) throws IOException - { - byte msg[] = new byte[5]; - - synchronized (c) - { - if (force) - { - c.state = Channel.STATE_CLOSED; - c.EOF = true; - } - - c.setReasonClosed(reason); - - msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE; - msg[1] = (byte) (c.remoteID >> 24); - msg[2] = (byte) (c.remoteID >> 16); - msg[3] = (byte) (c.remoteID >> 8); - msg[4] = (byte) (c.remoteID); - - c.notifyAll(); - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent == true) - return; - tm.sendMessage(msg); - c.closeMessageSent = true; - } - - log.debug("Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID + ")"); - } - - public void sendEOF(Channel c) throws IOException - { - byte[] msg = new byte[5]; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - return; - - msg[0] = Packets.SSH_MSG_CHANNEL_EOF; - msg[1] = (byte) (c.remoteID >> 24); - msg[2] = (byte) (c.remoteID >> 16); - msg[3] = (byte) (c.remoteID >> 8); - msg[4] = (byte) (c.remoteID); - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent == true) - return; - tm.sendMessage(msg); - } - - - log.debug("Sent EOF (Channel " + c.localID + "/" + c.remoteID + ")"); - } - - public void sendOpenConfirmation(Channel c) throws IOException - { - PacketChannelOpenConfirmation pcoc = null; - - synchronized (c) - { - if (c.state != Channel.STATE_OPENING) - return; - - c.state = Channel.STATE_OPEN; - - pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, c.localWindow, c.localMaxPacketSize); - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent == true) - return; - tm.sendMessage(pcoc.getPayload()); - } - } - - public void sendData(Channel c, byte[] buffer, int pos, int len) throws IOException - { - boolean wasInterrupted = false; - - try - { - while (len > 0) - { - int thislen = 0; - byte[] msg; - - synchronized (c) - { - while (true) - { - if (c.state == Channel.STATE_CLOSED) - throw new ChannelClosedException("SSH channel is closed. (" + c.getReasonClosed() + ")"); - - if (c.state != Channel.STATE_OPEN) - throw new ChannelClosedException("SSH channel in strange state. (" + c.state + ")"); - - if (c.remoteWindow != 0) - break; - - try - { - c.wait(); - } - catch (InterruptedException ignore) - { - wasInterrupted = true; - } - } - - /* len > 0, no sign extension can happen when comparing */ - - thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow; - - int estimatedMaxDataLen = c.remoteMaxPacketSize - (tm.getPacketOverheadEstimate() + 9); - - /* The worst case scenario =) a true bottleneck */ - - if (estimatedMaxDataLen <= 0) - { - estimatedMaxDataLen = 1; - } - - if (thislen > estimatedMaxDataLen) - thislen = estimatedMaxDataLen; - - c.remoteWindow -= thislen; - - msg = new byte[1 + 8 + thislen]; - - msg[0] = Packets.SSH_MSG_CHANNEL_DATA; - msg[1] = (byte) (c.remoteID >> 24); - msg[2] = (byte) (c.remoteID >> 16); - msg[3] = (byte) (c.remoteID >> 8); - msg[4] = (byte) (c.remoteID); - msg[5] = (byte) (thislen >> 24); - msg[6] = (byte) (thislen >> 16); - msg[7] = (byte) (thislen >> 8); - msg[8] = (byte) (thislen); - - System.arraycopy(buffer, pos, msg, 9, thislen); - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent == true) - throw new ChannelClosedException("SSH channel is closed. (" + c.getReasonClosed() + ")"); - - tm.sendMessage(msg); - } - - pos += thislen; - len -= thislen; - } - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - } - - public int requestGlobalForward(String bindAddress, int bindPort, String targetAddress, int targetPort) - throws IOException - { - RemoteForwardingData rfd = new RemoteForwardingData(); - - rfd.bindAddress = bindAddress; - rfd.bindPort = bindPort; - rfd.targetAddress = targetAddress; - rfd.targetPort = targetPort; - - synchronized (remoteForwardings) - { - Integer key = new Integer(bindPort); - - if (remoteForwardings.get(key) != null) - { - throw new IOException("There is already a forwarding for remote port " + bindPort); - } - - remoteForwardings.put(key, rfd); - } - - synchronized (channels) - { - globalSuccessCounter = globalFailedCounter = 0; - } - - PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true, bindAddress, bindPort); - tm.sendMessage(pgf.getPayload()); - - log.debug("Requesting a remote forwarding ('" + bindAddress + "', " + bindPort + ")"); - - try - { - waitForGlobalSuccessOrFailure(); - } - catch (IOException e) - { - synchronized (remoteForwardings) - { - remoteForwardings.remove(rfd); - } - throw e; - } - - return bindPort; - } - - public void requestCancelGlobalForward(int bindPort) throws IOException - { - RemoteForwardingData rfd = null; - - synchronized (remoteForwardings) - { - rfd = remoteForwardings.get(new Integer(bindPort)); - - if (rfd == null) - throw new IOException("Sorry, there is no known remote forwarding for remote port " + bindPort); - } - - synchronized (channels) - { - globalSuccessCounter = globalFailedCounter = 0; - } - - PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(true, rfd.bindAddress, - rfd.bindPort); - tm.sendMessage(pgcf.getPayload()); - - log.debug("Requesting cancelation of remote forward ('" + rfd.bindAddress + "', " + rfd.bindPort + ")"); - - waitForGlobalSuccessOrFailure(); - - /* Only now we are sure that no more forwarded connections will arrive */ - - synchronized (remoteForwardings) - { - remoteForwardings.remove(rfd); - } - } - - public void registerThread(IChannelWorkerThread thr) throws IOException - { - synchronized (listenerThreads) - { - if (listenerThreadsAllowed == false) - throw new IOException("Too late, this connection is closed."); - listenerThreads.add(thr); - } - } - - public Channel openDirectTCPIPChannel(String host_to_connect, int port_to_connect, String originator_IP_address, - int originator_port) throws IOException - { - Channel c = new Channel(this); - - synchronized (c) - { - c.localID = addChannel(c); - // end of synchronized block forces writing out to main memory - } - - PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(c.localID, c.localWindow, - c.localMaxPacketSize, host_to_connect, port_to_connect, originator_IP_address, originator_port); - - tm.sendMessage(dtc.getPayload()); - - waitUntilChannelOpen(c); - - return c; - } - - public Channel openSessionChannel() throws IOException - { - Channel c = new Channel(this); - - synchronized (c) - { - c.localID = addChannel(c); - // end of synchronized block forces the writing out to main memory - } - - log.debug("Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID + ")"); - - PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID, c.localWindow, c.localMaxPacketSize); - tm.sendMessage(smo.getPayload()); - - waitUntilChannelOpen(c); - - return c; - } - - public void requestPTY(Channel c, String term, int term_width_characters, int term_height_characters, - int term_width_pixels, int term_height_pixels, byte[] terminal_modes) throws IOException - { - PacketSessionPtyRequest spr; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); - - spr = new PacketSessionPtyRequest(c.remoteID, true, term, term_width_characters, term_height_characters, - term_width_pixels, term_height_pixels, terminal_modes); - - c.successCounter = c.failedCounter = 0; - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent) - throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); - tm.sendMessage(spr.getPayload()); - } - - try - { - waitForChannelSuccessOrFailure(c); - } - catch (IOException e) - { - throw (IOException) new IOException("PTY request failed").initCause(e); - } - } - - public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol, - String x11AuthenticationCookie, int x11ScreenNumber) throws IOException - { - PacketSessionX11Request psr; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); - - psr = new PacketSessionX11Request(c.remoteID, true, singleConnection, x11AuthenticationProtocol, - x11AuthenticationCookie, x11ScreenNumber); - - c.successCounter = c.failedCounter = 0; - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent) - throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); - tm.sendMessage(psr.getPayload()); - } - - log.debug("Requesting X11 forwarding (Channel " + c.localID + "/" + c.remoteID + ")"); - - try - { - waitForChannelSuccessOrFailure(c); - } - catch (IOException e) - { - throw (IOException) new IOException("The X11 request failed.").initCause(e); - } - } - - public void requestSubSystem(Channel c, String subSystemName) throws IOException - { - PacketSessionSubsystemRequest ssr; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); - - ssr = new PacketSessionSubsystemRequest(c.remoteID, true, subSystemName); - - c.successCounter = c.failedCounter = 0; - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent) - throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); - tm.sendMessage(ssr.getPayload()); - } - - try - { - waitForChannelSuccessOrFailure(c); - } - catch (IOException e) - { - throw (IOException) new IOException("The subsystem request failed.").initCause(e); - } - } - - public void requestExecCommand(Channel c, String cmd) throws IOException - { - this.requestExecCommand(c, cmd, null); - } - - /** - * @param charsetName The charset used to convert between Java Unicode Strings and byte encodings - */ - public void requestExecCommand(Channel c, String cmd, String charsetName) throws IOException - { - PacketSessionExecCommand sm; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); - - sm = new PacketSessionExecCommand(c.remoteID, true, cmd); - - c.successCounter = c.failedCounter = 0; - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent) - throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); - tm.sendMessage(sm.getPayload(charsetName)); - } - - log.debug("Executing command (channel " + c.localID + ", '" + cmd + "')"); - - try - { - waitForChannelSuccessOrFailure(c); - } - catch (IOException e) - { - throw (IOException) new IOException("The execute request failed.").initCause(e); - } - } - - public void requestShell(Channel c) throws IOException - { - PacketSessionStartShell sm; - - synchronized (c) - { - if (c.state != Channel.STATE_OPEN) - throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); - - sm = new PacketSessionStartShell(c.remoteID, true); - - c.successCounter = c.failedCounter = 0; - } - - synchronized (c.channelSendLock) - { - if (c.closeMessageSent) - throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); - tm.sendMessage(sm.getPayload()); - } - - try - { - waitForChannelSuccessOrFailure(c); - } - catch (IOException e) - { - throw (IOException) new IOException("The shell request failed.").initCause(e); - } - } - - public void msgChannelExtendedData(byte[] msg, int msglen) throws IOException - { - if (msglen <= 13) - throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); - int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16) | ((msg[11] & 0xff) << 8) | (msg[12] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel " + id); - - if (dataType != Packets.SSH_EXTENDED_DATA_STDERR) - throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type (" + dataType + ")"); - - if (len != (msglen - 13)) - throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated " + (msglen - 13) - + ", got " + len + ")"); - - log.debug("Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id + ", " + len + ")"); - - synchronized (c) - { - if (c.state == Channel.STATE_CLOSED) - return; // ignore - - if (c.state != Channel.STATE_OPEN) - throw new IOException("Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state (" - + c.state + ")"); - - if (c.localWindow < len) - throw new IOException("Remote sent too much data, does not fit into window."); - - c.localWindow -= len; - - System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len); - c.stderrWritepos += len; - - c.notifyAll(); - } - } - - /** - * Wait until for a condition. - * - * @param c Channel - * @param timeout in ms, 0 means no timeout. - * @param condition_mask minimum event mask (at least one of the conditions must be fulfilled) - * @return all current events - */ - public int waitForCondition(Channel c, long timeout, int condition_mask) - { - boolean wasInterrupted = false; - - try - { - long end_time = 0; - boolean end_time_set = false; - - synchronized (c) - { - while (true) - { - int current_cond = 0; - - int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; - int stderrAvail = c.stderrWritepos - c.stderrReadpos; - - if (stdoutAvail > 0) - current_cond = current_cond | ChannelCondition.STDOUT_DATA; - - if (stderrAvail > 0) - current_cond = current_cond | ChannelCondition.STDERR_DATA; - - if (c.EOF) - current_cond = current_cond | ChannelCondition.EOF; - - if (c.getExitStatus() != null) - current_cond = current_cond | ChannelCondition.EXIT_STATUS; - - if (c.getExitSignal() != null) - current_cond = current_cond | ChannelCondition.EXIT_SIGNAL; - - if (c.state == Channel.STATE_CLOSED) - return current_cond | ChannelCondition.CLOSED | ChannelCondition.EOF; - - if ((current_cond & condition_mask) != 0) - return current_cond; - - if (timeout > 0) - { - if (!end_time_set) - { - end_time = System.currentTimeMillis() + timeout; - end_time_set = true; - } - else - { - timeout = end_time - System.currentTimeMillis(); - - if (timeout <= 0) - return current_cond | ChannelCondition.TIMEOUT; - } - } - - try - { - if (timeout > 0) - c.wait(timeout); - else - c.wait(); - } - catch (InterruptedException e) - { - wasInterrupted = true; - } - } - } - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - } - - public int getAvailable(Channel c, boolean extended) throws IOException - { - synchronized (c) - { - int avail; - - if (extended) - avail = c.stderrWritepos - c.stderrReadpos; - else - avail = c.stdoutWritepos - c.stdoutReadpos; - - return ((avail > 0) ? avail : (c.EOF ? -1 : 0)); - } - } - - public int getChannelData(Channel c, boolean extended, byte[] target, int off, int len) throws IOException - { - boolean wasInterrupted = false; - - try - { - int copylen = 0; - int increment = 0; - int remoteID = 0; - int localID = 0; - - synchronized (c) - { - int stdoutAvail = 0; - int stderrAvail = 0; - - while (true) - { - /* - * Data available? We have to return remaining data even if the - * channel is already closed. - */ - - stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; - stderrAvail = c.stderrWritepos - c.stderrReadpos; - - if ((!extended) && (stdoutAvail != 0)) - break; - - if ((extended) && (stderrAvail != 0)) - break; - - /* Do not wait if more data will never arrive (EOF or CLOSED) */ - - if ((c.EOF) || (c.state != Channel.STATE_OPEN)) - return -1; - - try - { - c.wait(); - } - catch (InterruptedException ignore) - { - wasInterrupted = true; - } - } - - /* OK, there is some data. Return it. */ - - if (!extended) - { - copylen = (stdoutAvail > len) ? len : stdoutAvail; - System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off, copylen); - c.stdoutReadpos += copylen; - - if (c.stdoutReadpos != c.stdoutWritepos) - - System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, c.stdoutBuffer, 0, c.stdoutWritepos - - c.stdoutReadpos); - - c.stdoutWritepos -= c.stdoutReadpos; - c.stdoutReadpos = 0; - } - else - { - copylen = (stderrAvail > len) ? len : stderrAvail; - System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off, copylen); - c.stderrReadpos += copylen; - - if (c.stderrReadpos != c.stderrWritepos) - - System.arraycopy(c.stderrBuffer, c.stderrReadpos, c.stderrBuffer, 0, c.stderrWritepos - - c.stderrReadpos); - - c.stderrWritepos -= c.stderrReadpos; - c.stderrReadpos = 0; - } - - if (c.state != Channel.STATE_OPEN) - return copylen; - - if (c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2)) - { - int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE - c.stdoutWritepos, - Channel.CHANNEL_BUFFER_SIZE - c.stderrWritepos); - - increment = minFreeSpace - c.localWindow; - c.localWindow = minFreeSpace; - } - - remoteID = c.remoteID; /* read while holding the lock */ - localID = c.localID; /* read while holding the lock */ - } - - /* - * If a consumer reads stdout and stdin in parallel, we may end up with - * sending two msgWindowAdjust messages. Luckily, it - * does not matter in which order they arrive at the server. - */ - - if (increment > 0) - { - log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")"); - - synchronized (c.channelSendLock) - { - byte[] msg = c.msgWindowAdjust; - - msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST; - msg[1] = (byte) (remoteID >> 24); - msg[2] = (byte) (remoteID >> 16); - msg[3] = (byte) (remoteID >> 8); - msg[4] = (byte) (remoteID); - msg[5] = (byte) (increment >> 24); - msg[6] = (byte) (increment >> 16); - msg[7] = (byte) (increment >> 8); - msg[8] = (byte) (increment); - - if (c.closeMessageSent == false) - tm.sendMessage(msg); - } - } - - return copylen; - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - - } - - public void msgChannelData(byte[] msg, int msglen) throws IOException - { - if (msglen <= 9) - throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel " + id); - - if (len != (msglen - 9)) - throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong len (calculated " + (msglen - 9) + ", got " - + len + ")"); - - log.debug("Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len + ")"); - - synchronized (c) - { - if (c.state == Channel.STATE_CLOSED) - return; // ignore - - if (c.state != Channel.STATE_OPEN) - throw new IOException("Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state (" + c.state + ")"); - - if (c.localWindow < len) - throw new IOException("Remote sent too much data, does not fit into window."); - - c.localWindow -= len; - - System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len); - c.stdoutWritepos += len; - - c.notifyAll(); - } - } - - public void msgChannelWindowAdjust(byte[] msg, int msglen) throws IOException - { - if (msglen != 9) - throw new IOException("SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel " + id); - - synchronized (c) - { - final long huge = 0xFFFFffffL; /* 2^32 - 1 */ - - c.remoteWindow += (windowChange & huge); /* avoid sign extension */ - - /* TODO - is this a good heuristic? */ - - if ((c.remoteWindow > huge)) - c.remoteWindow = huge; - - c.notifyAll(); - } - - - log.debug("Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id + ", " + windowChange + ")"); - } - - public void msgChannelOpen(byte[] msg, int msglen) throws IOException - { - TypesReader tr = new TypesReader(msg, 0, msglen); - - tr.readByte(); // skip packet type - String channelType = tr.readString(); - int remoteID = tr.readUINT32(); /* sender channel */ - int remoteWindow = tr.readUINT32(); /* initial window size */ - int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */ - - if ("x11".equals(channelType)) - { - synchronized (x11_magic_cookies) - { - /* If we did not request X11 forwarding, then simply ignore this bogus request. */ - - if (x11_magic_cookies.size() == 0) - { - PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, - Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "X11 forwarding not activated", ""); - - tm.sendAsynchronousMessage(pcof.getPayload()); - - log.warning("Unexpected X11 request, denying it!"); - - return; - } - } - - String remoteOriginatorAddress = tr.readString(); - int remoteOriginatorPort = tr.readUINT32(); - - Channel c = new Channel(this); - - synchronized (c) - { - c.remoteID = remoteID; - c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ - c.remoteMaxPacketSize = remoteMaxPacketSize; - c.localID = addChannel(c); - } - - /* - * The open confirmation message will be sent from another thread - */ - - RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c, remoteOriginatorAddress, remoteOriginatorPort); - rxat.setDaemon(true); - rxat.start(); - - return; - } - - if ("forwarded-tcpip".equals(channelType)) - { - String remoteConnectedAddress = tr.readString(); /* address that was connected */ - int remoteConnectedPort = tr.readUINT32(); /* port that was connected */ - String remoteOriginatorAddress = tr.readString(); /* originator IP address */ - int remoteOriginatorPort = tr.readUINT32(); /* originator port */ - - RemoteForwardingData rfd = null; - - synchronized (remoteForwardings) - { - rfd = remoteForwardings.get(new Integer(remoteConnectedPort)); - } - - if (rfd == null) - { - PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, - Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, - "No thanks, unknown port in forwarded-tcpip request", ""); - - /* Always try to be polite. */ - - tm.sendAsynchronousMessage(pcof.getPayload()); - - log.debug("Unexpected forwarded-tcpip request, denying it!"); - - return; - } - - Channel c = new Channel(this); - - synchronized (c) - { - c.remoteID = remoteID; - c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */ - c.remoteMaxPacketSize = remoteMaxPacketSize; - c.localID = addChannel(c); - } - - /* - * The open confirmation message will be sent from another thread. - */ - - RemoteAcceptThread rat = new RemoteAcceptThread(c, remoteConnectedAddress, remoteConnectedPort, - remoteOriginatorAddress, remoteOriginatorPort, rfd.targetAddress, rfd.targetPort); - - rat.setDaemon(true); - rat.start(); - - return; - } - - if ((server_state != null) && ("session".equals(channelType))) - { - ServerConnectionCallback cb = null; - - synchronized (server_state) - { - cb = server_state.cb_conn; - } - - if (cb == null) - { - tm.sendAsynchronousMessage(new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, - "Sessions are currently not enabled", "en").getPayload()); - - return; - } - - final Channel c = new Channel(this); - - synchronized (c) - { - c.remoteID = remoteID; - c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */ - c.remoteMaxPacketSize = remoteMaxPacketSize; - c.localID = addChannel(c); - c.state = Channel.STATE_OPEN; - c.ss = new ServerSessionImpl(c); - } - - PacketChannelOpenConfirmation pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, - c.localWindow, c.localMaxPacketSize); - - tm.sendAsynchronousMessage(pcoc.getPayload()); - - c.ss.sscb = cb.acceptSession(c.ss); - - return; - } - - /* Tell the server that we have no idea what it is talking about */ - - PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, - "Unknown channel type", ""); - - tm.sendAsynchronousMessage(pcof.getPayload()); - - - log.warning("The peer tried to open an unsupported channel type (" + channelType + ")"); - } - - /* Starts the given runnable in a foreground (non-daemon) thread */ - private void runAsync(Runnable r) - { - Thread t = new Thread(r); - t.start(); - } - - public void msgChannelRequest(byte[] msg, int msglen) throws IOException - { - TypesReader tr = new TypesReader(msg, 0, msglen); - - tr.readByte(); // skip packet type - int id = tr.readUINT32(); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel " + id); - - ServerSessionImpl server_session = null; - - if (server_state != null) - { - synchronized (c) - { - server_session = c.ss; - } - } - - String type = tr.readString("US-ASCII"); - boolean wantReply = tr.readBoolean(); - - log.debug("Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '" + type + "')"); - - if (type.equals("exit-status")) - { - if (wantReply != false) - throw new IOException( - "Badly formatted SSH_MSG_CHANNEL_REQUEST exit-status message, 'want reply' is true"); - - int exit_status = tr.readUINT32(); - - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - synchronized (c) - { - c.exit_status = new Integer(exit_status); - c.notifyAll(); - } - - log.debug("Got EXIT STATUS (channel " + id + ", status " + exit_status + ")"); - - return; - } - - if ((server_state == null) && (type.equals("exit-signal"))) - { - if (wantReply != false) - throw new IOException( - "Badly formatted SSH_MSG_CHANNEL_REQUEST exit-signal message, 'want reply' is true"); - - String signame = tr.readString("US-ASCII"); - tr.readBoolean(); - tr.readString(); - tr.readString(); - - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - synchronized (c) - { - c.exit_signal = signame; - c.notifyAll(); - } - - log.debug("Got EXIT SIGNAL (channel " + id + ", signal " + signame + ")"); - - return; - } - - if ((server_session != null) && (type.equals("pty-req"))) - { - PtySettings pty = new PtySettings(); - - pty.term = tr.readString(); - pty.term_width_characters = tr.readUINT32(); - pty.term_height_characters = tr.readUINT32(); - pty.term_width_pixels = tr.readUINT32(); - pty.term_height_pixels = tr.readUINT32(); - pty.terminal_modes = tr.readByteString(); - - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - Runnable run_after_sending_success = null; - - ServerSessionCallback sscb = server_session.getServerSessionCallback(); - - if (sscb != null) - run_after_sending_success = sscb.requestPtyReq(server_session, pty); - - if (wantReply) - { - if (run_after_sending_success != null) - { - tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload()); - } - else - { - tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload()); - } - } - - if (run_after_sending_success != null) - { - runAsync(run_after_sending_success); - } - - return; - } - - if ((server_session != null) && (type.equals("subsystem"))) - { - String command = tr.readString(); - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - Runnable run_after_sending_success = null; - ServerSessionCallback sscb = server_session.getServerSessionCallback(); - - if (sscb != null) - run_after_sending_success = sscb.requestSubsystem(server_session, command); - - if (wantReply) - { - if (run_after_sending_success != null) - { - tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload()); - } - else - { - tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload()); - } - } - - if (run_after_sending_success != null) - { - runAsync(run_after_sending_success); - } - - return; - } - - if ((server_session != null) && (type.equals("shell"))) - { - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - Runnable run_after_sending_success = null; - ServerSessionCallback sscb = server_session.getServerSessionCallback(); - - if (sscb != null) - run_after_sending_success = sscb.requestShell(server_session); - - if (wantReply) - { - if (run_after_sending_success != null) - { - tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload()); - } - else - { - tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload()); - } - } - - if (run_after_sending_success != null) - { - runAsync(run_after_sending_success); - } - - return; - } - - if ((server_session != null) && (type.equals("exec"))) - { - String command = tr.readString(); - - if (tr.remain() != 0) - throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); - - Runnable run_after_sending_success = null; - ServerSessionCallback sscb = server_session.getServerSessionCallback(); - - if (sscb != null) - run_after_sending_success = sscb.requestExec(server_session, command); - - if (wantReply) - { - if (run_after_sending_success != null) - { - tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload()); - } - else - { - tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload()); - } - } - - if (run_after_sending_success != null) - { - runAsync(run_after_sending_success); - } - - return; - } - - /* We simply ignore unknown channel requests, however, if the server wants a reply, - * then we signal that we have no idea what it is about. - */ - - if (wantReply) - { - tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload()); - } - - log.debug("Channel request '" + type + "' is not known, ignoring it"); - } - - public void msgChannelEOF(byte[] msg, int msglen) throws IOException - { - if (msglen != 5) - throw new IOException("SSH_MSG_CHANNEL_EOF message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel " + id); - - synchronized (c) - { - c.EOF = true; - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_EOF (channel " + id + ")"); - } - - public void msgChannelClose(byte[] msg, int msglen) throws IOException - { - if (msglen != 5) - throw new IOException("SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel " + id); - - synchronized (c) - { - c.EOF = true; - c.state = Channel.STATE_CLOSED; - c.setReasonClosed("Close requested by remote"); - c.closeMessageRecv = true; - - removeChannel(c.localID); - - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")"); - } - - public void msgChannelSuccess(byte[] msg, int msglen) throws IOException - { - if (msglen != 5) - throw new IOException("SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel " + id); - - synchronized (c) - { - c.successCounter++; - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")"); - } - - public void msgChannelFailure(byte[] msg, int msglen) throws IOException - { - if (msglen != 5) - throw new IOException("SSH_MSG_CHANNEL_FAILURE message has wrong size (" + msglen + ")"); - - int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel " + id); - - synchronized (c) - { - c.failedCounter++; - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")"); - } - - public void msgChannelOpenConfirmation(byte[] msg, int msglen) throws IOException - { - PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(msg, 0, msglen); - - Channel c = getChannel(sm.recipientChannelID); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel " - + sm.recipientChannelID); - - synchronized (c) - { - if (c.state != Channel.STATE_OPENING) - throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel " - + sm.recipientChannelID); - - c.remoteID = sm.senderChannelID; - c.remoteWindow = sm.initialWindowSize & 0xFFFFffffL; /* convert UINT32 to long */ - c.remoteMaxPacketSize = sm.maxPacketSize; - c.state = Channel.STATE_OPEN; - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel " + sm.recipientChannelID + " / remote: " - + sm.senderChannelID + ")"); - } - - public void msgChannelOpenFailure(byte[] msg, int msglen) throws IOException - { - if (msglen < 5) - throw new IOException("SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size (" + msglen + ")"); - - TypesReader tr = new TypesReader(msg, 0, msglen); - - tr.readByte(); // skip packet type - int id = tr.readUINT32(); /* sender channel */ - - Channel c = getChannel(id); - - if (c == null) - throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel " + id); - - int reasonCode = tr.readUINT32(); - String description = tr.readString("UTF-8"); - - String reasonCodeSymbolicName = null; - - switch (reasonCode) - { - case 1: - reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED"; - break; - case 2: - reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED"; - break; - case 3: - reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE"; - break; - case 4: - reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE"; - break; - default: - reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")"; - } - - StringBuilder descriptionBuffer = new StringBuilder(); - descriptionBuffer.append(description); - - for (int i = 0; i < descriptionBuffer.length(); i++) - { - char cc = descriptionBuffer.charAt(i); - - if ((cc >= 32) && (cc <= 126)) - continue; - descriptionBuffer.setCharAt(i, '\uFFFD'); - } - - synchronized (c) - { - c.EOF = true; - c.state = Channel.STATE_CLOSED; - c.setReasonClosed("The server refused to open the channel (" + reasonCodeSymbolicName + ", '" - + descriptionBuffer.toString() + "')"); - c.notifyAll(); - } - - log.debug("Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")"); - } - - public void msgGlobalRequest(byte[] msg, int msglen) throws IOException - { - /* Currently we do not support any kind of global request */ - - TypesReader tr = new TypesReader(msg, 0, msglen); - - tr.readByte(); // skip packet type - String requestName = tr.readString(); - boolean wantReply = tr.readBoolean(); - - if (wantReply) - { - byte[] reply_failure = new byte[1]; - reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE; - - tm.sendAsynchronousMessage(reply_failure); - } - - /* We do not clean up the requestName String - that is OK for debug */ - - log.debug("Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")"); - } - - public void msgGlobalSuccess() throws IOException - { - synchronized (channels) - { - globalSuccessCounter++; - channels.notifyAll(); - } - - log.debug("Got SSH_MSG_REQUEST_SUCCESS"); - } - - public void msgGlobalFailure() throws IOException - { - synchronized (channels) - { - globalFailedCounter++; - channels.notifyAll(); - } - - log.debug("Got SSH_MSG_REQUEST_FAILURE"); - } - - public void handleMessage(byte[] msg, int msglen) throws IOException - { - if (msg == null) - { - - log.debug("HandleMessage: got shutdown"); - - synchronized (listenerThreads) - { - for (IChannelWorkerThread lat : listenerThreads) - { - lat.stopWorking(); - } - listenerThreadsAllowed = false; - } - - synchronized (channels) - { - shutdown = true; - - for (Channel c : channels) - { - synchronized (c) - { - c.EOF = true; - c.state = Channel.STATE_CLOSED; - c.setReasonClosed("The connection is being shutdown"); - c.closeMessageRecv = true; /* - * You never know, perhaps - * we are waiting for a - * pending close message - * from the server... - */ - c.notifyAll(); - } - } - - channels.clear(); - channels.notifyAll(); /* Notify global response waiters */ - return; - } - } - - switch (msg[0]) - { - case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION: - msgChannelOpenConfirmation(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST: - msgChannelWindowAdjust(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_DATA: - msgChannelData(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA: - msgChannelExtendedData(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_REQUEST: - msgChannelRequest(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_EOF: - msgChannelEOF(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_OPEN: - msgChannelOpen(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_CLOSE: - msgChannelClose(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_SUCCESS: - msgChannelSuccess(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_FAILURE: - msgChannelFailure(msg, msglen); - break; - case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE: - msgChannelOpenFailure(msg, msglen); - break; - case Packets.SSH_MSG_GLOBAL_REQUEST: - msgGlobalRequest(msg, msglen); - break; - case Packets.SSH_MSG_REQUEST_SUCCESS: - msgGlobalSuccess(); - break; - case Packets.SSH_MSG_REQUEST_FAILURE: - msgGlobalFailure(); - break; - default: - throw new IOException("Cannot handle unknown channel message " + (msg[0] & 0xff)); - } - } -} diff --git a/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java b/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java deleted file mode 100644 index 963267082b..0000000000 --- a/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java +++ /dev/null @@ -1,990 +0,0 @@ -/* - * Copyright (c) 2006-2013 Christian Plattner. All rights reserved. - * Please refer to the LICENSE.txt for licensing details. - */ - -package ch.ethz.ssh2.transport; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.security.SecureRandom; -import java.util.List; -import java.util.Vector; - -import ch.ethz.ssh2.ConnectionInfo; -import ch.ethz.ssh2.ConnectionMonitor; -import ch.ethz.ssh2.DHGexParameters; -import ch.ethz.ssh2.HTTPProxyData; -import ch.ethz.ssh2.HTTPProxyException; -import ch.ethz.ssh2.ProxyData; -import ch.ethz.ssh2.ServerHostKeyVerifier; -import ch.ethz.ssh2.crypto.Base64; -import ch.ethz.ssh2.crypto.CryptoWishList; -import ch.ethz.ssh2.crypto.cipher.BlockCipher; -import ch.ethz.ssh2.crypto.digest.MAC; -import ch.ethz.ssh2.log.Logger; -import ch.ethz.ssh2.packets.PacketDisconnect; -import ch.ethz.ssh2.packets.Packets; -import ch.ethz.ssh2.packets.TypesReader; -import ch.ethz.ssh2.server.ServerConnectionState; -import ch.ethz.ssh2.signature.DSAPrivateKey; -import ch.ethz.ssh2.signature.RSAPrivateKey; -import ch.ethz.ssh2.util.StringEncoder; -import ch.ethz.ssh2.util.Tokenizer; - -/* - * Yes, the "standard" is a big mess. On one side, the say that arbitary channel - * packets are allowed during kex exchange, on the other side we need to blindly - * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that - * the next packet is not a channel data packet? Yes, we could check if it is in - * the KEX range. But the standard says nothing about this. The OpenSSH guys - * block local "normal" traffic during KEX. That's fine - however, they assume - * that the other side is doing the same. During re-key, if they receive traffic - * other than KEX, they become horribly irritated and kill the connection. Since - * we are very likely going to communicate with OpenSSH servers, we have to play - * the same game - even though we could do better. - * - * btw: having stdout and stderr on the same channel, with a shared window, is - * also a VERY good idea... =( - */ - -/** - * TransportManager. - * - * @author Christian Plattner - * @version $Id: TransportManager.java 47 2013-07-31 23:59:52Z cleondris@gmail.com $ - */ -public class TransportManager -{ - private static final Logger log = Logger.getLogger(TransportManager.class); - - private static class HandlerEntry - { - MessageHandler mh; - int low; - int high; - } - - private final List asynchronousQueue = new Vector(); - private Thread asynchronousThread = null; - private boolean asynchronousPending = false; - - class AsynchronousEntry - { - public byte[] msg; - public Runnable run; - - public AsynchronousEntry(byte[] msg, Runnable run) - { - this.msg = msg; - this.run = run; - } - } - - class AsynchronousWorker extends Thread - { - @Override - public void run() - { - while (true) - { - AsynchronousEntry item = null; - - synchronized (asynchronousQueue) - { - if (asynchronousQueue.size() == 0) - { - /* Only now we may reset the flag, since we are sure that all queued items - * have been sent (there is a slight delay between de-queuing and sending, - * this is why we need this flag! See code below. Sending takes place outside - * of this lock, this is why a test for size()==0 (from another thread) does not ensure - * that all messages have been sent. - */ - - asynchronousPending = false; - - /* Notify any senders that they can proceed, all async messages have been delivered */ - - asynchronousQueue.notifyAll(); - - /* After the queue is empty for about 2 seconds, stop this thread */ - - try - { - asynchronousQueue.wait(2000); - } - catch (InterruptedException ignore) - { - } - - if (asynchronousQueue.size() == 0) - { - asynchronousThread = null; - return; - } - } - - item = asynchronousQueue.remove(0); - } - - /* The following invocation may throw an IOException. - * There is no point in handling it - it simply means - * that the connection has a problem and we should stop - * sending asynchronously messages. We do not need to signal that - * we have exited (asynchronousThread = null): further - * messages in the queue cannot be sent by this or any - * other thread. - * Other threads will sooner or later (when receiving or - * sending the next message) get the same IOException and - * get to the same conclusion. - */ - - try - { - sendMessageImmediate(item.msg); - } - catch (IOException e) - { - return; - } - - if (item.run != null) - { - try - { - item.run.run(); - } - catch (Exception ignore) - { - } - - } - } - } - } - - private Socket sock = new Socket(); - - private final Object connectionSemaphore = new Object(); - - private boolean flagKexOngoing = false; - private boolean connectionClosed = false; - - private Throwable reasonClosedCause = null; - - private TransportConnection tc; - private KexManager km; - - private final List messageHandlers = new Vector(); - - private Thread receiveThread; - - private List connectionMonitors = new Vector(); - private boolean monitorsWereInformed = false; - - /** - * There were reports that there are JDKs which use - * the resolver even though one supplies a dotted IP - * address in the Socket constructor. That is why we - * try to generate the InetAdress "by hand". - * - * @param host - * @return the InetAddress - * @throws UnknownHostException - */ - private static InetAddress createInetAddress(String host) throws UnknownHostException - { - /* Check if it is a dotted IP4 address */ - - InetAddress addr = parseIPv4Address(host); - - if (addr != null) - { - return addr; - } - - return InetAddress.getByName(host); - } - - private static InetAddress parseIPv4Address(String host) throws UnknownHostException - { - if (host == null) - { - return null; - } - - String[] quad = Tokenizer.parseTokens(host, '.'); - - if ((quad == null) || (quad.length != 4)) - { - return null; - } - - byte[] addr = new byte[4]; - - for (int i = 0; i < 4; i++) - { - int part = 0; - - if ((quad[i].length() == 0) || (quad[i].length() > 3)) - { - return null; - } - - for (int k = 0; k < quad[i].length(); k++) - { - char c = quad[i].charAt(k); - - /* No, Character.isDigit is not the same */ - if ((c < '0') || (c > '9')) - { - return null; - } - - part = part * 10 + (c - '0'); - } - - if (part > 255) /* 300.1.2.3 is invalid =) */ - { - return null; - } - - addr[i] = (byte) part; - } - - return InetAddress.getByAddress(host, addr); - } - - public int getPacketOverheadEstimate() - { - return tc.getPacketOverheadEstimate(); - } - - public void setTcpNoDelay(boolean state) throws IOException - { - sock.setTcpNoDelay(state); - } - - public void setSoTimeout(int timeout) throws IOException - { - sock.setSoTimeout(timeout); - } - - public ConnectionInfo getConnectionInfo(int kexNumber) throws IOException - { - return km.getOrWaitForConnectionInfo(kexNumber); - } - - public Throwable getReasonClosedCause() - { - synchronized (connectionSemaphore) - { - return reasonClosedCause; - } - } - - public byte[] getSessionIdentifier() - { - return km.sessionId; - } - - public void close(Throwable cause, boolean useDisconnectPacket) - { - if (useDisconnectPacket == false) - { - /* OK, hard shutdown - do not aquire the semaphore, - * perhaps somebody is inside (and waits until the remote - * side is ready to accept new data). */ - - try - { - sock.close(); - } - catch (IOException ignore) - { - } - - /* OK, whoever tried to send data, should now agree that - * there is no point in further waiting =) - * It is safe now to aquire the semaphore. - */ - } - - synchronized (connectionSemaphore) - { - if (connectionClosed == false) - { - if (useDisconnectPacket == true) - { - try - { - byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.getMessage(), "") - .getPayload(); - if (tc != null) - { - tc.sendMessage(msg); - } - } - catch (IOException ignore) - { - } - - try - { - sock.close(); - } - catch (IOException ignore) - { - } - } - - connectionClosed = true; - reasonClosedCause = cause; /* may be null */ - } - connectionSemaphore.notifyAll(); - } - - /* No check if we need to inform the monitors */ - - List monitors = new Vector(); - - synchronized (this) - { - /* Short term lock to protect "connectionMonitors" - * and "monitorsWereInformed" - * (they may be modified concurrently) - */ - - if (monitorsWereInformed == false) - { - monitorsWereInformed = true; - monitors.addAll(connectionMonitors); - } - } - - for (ConnectionMonitor cmon : monitors) - { - try - { - cmon.connectionLost(reasonClosedCause); - } - catch (Exception ignore) - { - } - } - } - - private static Socket establishConnection(String hostname, int port, ProxyData proxyData, int connectTimeout) - throws IOException - { - /* See the comment for createInetAddress() */ - - if (proxyData == null) - { - InetAddress addr = createInetAddress(hostname); - Socket s = new Socket(); - s.connect(new InetSocketAddress(addr, port), connectTimeout); - return s; - } - - if (proxyData instanceof HTTPProxyData) - { - HTTPProxyData pd = (HTTPProxyData) proxyData; - - /* At the moment, we only support HTTP proxies */ - - InetAddress addr = createInetAddress(pd.proxyHost); - Socket s = new Socket(); - s.connect(new InetSocketAddress(addr, pd.proxyPort), connectTimeout); - - /* OK, now tell the proxy where we actually want to connect to */ - - StringBuilder sb = new StringBuilder(); - - sb.append("CONNECT "); - sb.append(hostname); - sb.append(':'); - sb.append(port); - sb.append(" HTTP/1.0\r\n"); - - if ((pd.proxyUser != null) && (pd.proxyPass != null)) - { - String credentials = pd.proxyUser + ":" + pd.proxyPass; - char[] encoded = Base64.encode(StringEncoder.GetBytes(credentials)); - sb.append("Proxy-Authorization: Basic "); - sb.append(encoded); - sb.append("\r\n"); - } - - if (pd.requestHeaderLines != null) - { - for (int i = 0; i < pd.requestHeaderLines.length; i++) - { - if (pd.requestHeaderLines[i] != null) - { - sb.append(pd.requestHeaderLines[i]); - sb.append("\r\n"); - } - } - } - - sb.append("\r\n"); - - OutputStream out = s.getOutputStream(); - - out.write(StringEncoder.GetBytes(sb.toString())); - out.flush(); - - /* Now parse the HTTP response */ - - byte[] buffer = new byte[1024]; - InputStream in = s.getInputStream(); - - int len = ClientServerHello.readLineRN(in, buffer); - - String httpReponse = StringEncoder.GetString(buffer, 0, len); - - if (httpReponse.startsWith("HTTP/") == false) - { - throw new IOException("The proxy did not send back a valid HTTP response."); - } - - /* "HTTP/1.X XYZ X" => 14 characters minimum */ - - if ((httpReponse.length() < 14) || (httpReponse.charAt(8) != ' ') || (httpReponse.charAt(12) != ' ')) - { - throw new IOException("The proxy did not send back a valid HTTP response."); - } - - int errorCode = 0; - - try - { - errorCode = Integer.parseInt(httpReponse.substring(9, 12)); - } - catch (NumberFormatException ignore) - { - throw new IOException("The proxy did not send back a valid HTTP response."); - } - - if ((errorCode < 0) || (errorCode > 999)) - { - throw new IOException("The proxy did not send back a valid HTTP response."); - } - - if (errorCode != 200) - { - throw new HTTPProxyException(httpReponse.substring(13), errorCode); - } - - /* OK, read until empty line */ - - while (true) - { - len = ClientServerHello.readLineRN(in, buffer); - if (len == 0) - { - break; - } - } - return s; - } - - throw new IOException("Unsupported ProxyData"); - } - - private void startReceiver() throws IOException - { - receiveThread = new Thread(new Runnable() - { - public void run() - { - try - { - receiveLoop(); - } - catch (Exception e) - { - close(e, false); - - log.warning("Receive thread: error in receiveLoop: " + e.getMessage()); - } - - if (log.isDebugEnabled()) - { - log.debug("Receive thread: back from receiveLoop"); - } - - /* Tell all handlers that it is time to say goodbye */ - - if (km != null) - { - try - { - km.handleMessage(null, 0); - } - catch (IOException ignored) - { - } - } - - for (HandlerEntry he : messageHandlers) - { - try - { - he.mh.handleMessage(null, 0); - } - catch (Exception ignore) - { - } - } - } - }); - - receiveThread.setDaemon(true); - receiveThread.start(); - } - - public void clientInit(Socket socket, String softwareversion, CryptoWishList cwl, - ServerHostKeyVerifier verifier, DHGexParameters dhgex, SecureRandom rnd) throws IOException - { - /* First, establish the TCP connection to the SSH-2 server */ - - sock = socket; - - /* Parse the server line and say hello - important: this information is later needed for the - * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object - * for later use. - */ - - ClientServerHello csh = ClientServerHello.clientHello(softwareversion, sock.getInputStream(), - sock.getOutputStream()); - - tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); - String hostname = sock.getInetAddress().getHostName(); - int port = sock.getPort(); - - km = new ClientKexManager(this, csh, cwl, hostname, port, verifier, rnd); - km.initiateKEX(cwl, dhgex, null, null); - - startReceiver(); - } - - public void clientInit(String hostname, int port, String softwareversion, CryptoWishList cwl, - ServerHostKeyVerifier verifier, DHGexParameters dhgex, int connectTimeout, SecureRandom rnd, - ProxyData proxyData) throws IOException - { - /* First, establish the TCP connection to the SSH-2 server */ - - sock = establishConnection(hostname, port, proxyData, connectTimeout); - - /* Parse the server line and say hello - important: this information is later needed for the - * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object - * for later use. - */ - - ClientServerHello csh = ClientServerHello.clientHello(softwareversion, sock.getInputStream(), - sock.getOutputStream()); - - tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); - - km = new ClientKexManager(this, csh, cwl, hostname, port, verifier, rnd); - km.initiateKEX(cwl, dhgex, null, null); - - startReceiver(); - } - - public void serverInit(ServerConnectionState state) throws IOException - { - /* TCP connection is already established */ - - this.sock = state.s; - - /* Parse the client line and say hello - important: this information is later needed for the - * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object - * for later use. - */ - - state.csh = ClientServerHello.serverHello(state.softwareversion, sock.getInputStream(), sock.getOutputStream()); - - tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), state.generator); - - km = new ServerKexManager(state); - km.initiateKEX(state.next_cryptoWishList, null, state.next_dsa_key, state.next_rsa_key); - - startReceiver(); - } - - public void registerMessageHandler(MessageHandler mh, int low, int high) - { - HandlerEntry he = new HandlerEntry(); - he.mh = mh; - he.low = low; - he.high = high; - - synchronized (messageHandlers) - { - messageHandlers.add(he); - } - } - - public void removeMessageHandler(MessageHandler mh, int low, int high) - { - synchronized (messageHandlers) - { - for (int i = 0; i < messageHandlers.size(); i++) - { - HandlerEntry he = messageHandlers.get(i); - if ((he.mh == mh) && (he.low == low) && (he.high == high)) - { - messageHandlers.remove(i); - break; - } - } - } - } - - public void sendKexMessage(byte[] msg) throws IOException - { - synchronized (connectionSemaphore) - { - if (connectionClosed) - { - throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause); - } - - flagKexOngoing = true; - - try - { - tc.sendMessage(msg); - } - catch (IOException e) - { - close(e, false); - throw e; - } - } - } - - public void kexFinished() throws IOException - { - synchronized (connectionSemaphore) - { - flagKexOngoing = false; - connectionSemaphore.notifyAll(); - } - } - - /** - * - * @param cwl - * @param dhgex - * @param dsa may be null if this is a client connection - * @param rsa may be null if this is a client connection - * @throws IOException - */ - public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex, DSAPrivateKey dsa, RSAPrivateKey rsa) - throws IOException - { - synchronized (connectionSemaphore) - { - if (connectionClosed) - /* Inform the caller that there is no point in triggering a new kex */ - throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause); - } - - km.initiateKEX(cwl, dhgex, dsa, rsa); - } - - public void changeRecvCipher(BlockCipher bc, MAC mac) - { - tc.changeRecvCipher(bc, mac); - } - - public void changeSendCipher(BlockCipher bc, MAC mac) - { - tc.changeSendCipher(bc, mac); - } - - public void sendAsynchronousMessage(byte[] msg) throws IOException - { - sendAsynchronousMessage(msg, null); - } - - public void sendAsynchronousMessage(byte[] msg, Runnable run) throws IOException - { - synchronized (asynchronousQueue) - { - asynchronousQueue.add(new AsynchronousEntry(msg, run)); - asynchronousPending = true; - - /* This limit should be flexible enough. We need this, otherwise the peer - * can flood us with global requests (and other stuff where we have to reply - * with an asynchronous message) and (if the server just sends data and does not - * read what we send) this will probably put us in a low memory situation - * (our send queue would grow and grow and...) */ - - if (asynchronousQueue.size() > 100) - { - throw new IOException("Error: the peer is not consuming our asynchronous replies."); - } - - /* Check if we have an asynchronous sending thread */ - - if (asynchronousThread == null) - { - asynchronousThread = new AsynchronousWorker(); - asynchronousThread.setDaemon(true); - asynchronousThread.start(); - - /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */ - } - - asynchronousQueue.notifyAll(); - } - } - - public void setConnectionMonitors(List monitors) - { - synchronized (this) - { - connectionMonitors = new Vector(); - connectionMonitors.addAll(monitors); - } - } - - /** - * True if no response message expected. - */ - private boolean idle; - - /** - * Send a message but ensure that all queued messages are being sent first. - * - * @param msg - * @throws IOException - */ - public void sendMessage(byte[] msg) throws IOException - { - synchronized (asynchronousQueue) - { - while (asynchronousPending) - { - try - { - asynchronousQueue.wait(1000); - } - catch (InterruptedException e) - { - } - } - } - - sendMessageImmediate(msg); - } - - /** - * Send message, ignore queued async messages that have not been delivered yet. - * Will be called directly from the asynchronousThread thread. - * - * @param msg - * @throws IOException - */ - public void sendMessageImmediate(byte[] msg) throws IOException - { - if (Thread.currentThread() == receiveThread) - { - throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!"); - } - - boolean wasInterrupted = false; - - try - { - synchronized (connectionSemaphore) - { - while (true) - { - if (connectionClosed) - { - throw (IOException) new IOException("Sorry, this connection is closed.") - .initCause(reasonClosedCause); - } - - if (flagKexOngoing == false) - { - break; - } - - try - { - connectionSemaphore.wait(); - } - catch (InterruptedException e) - { - wasInterrupted = true; - } - } - - try - { - tc.sendMessage(msg); - idle = false; - } - catch (IOException e) - { - close(e, false); - throw e; - } - } - } - finally - { - if (wasInterrupted) - Thread.currentThread().interrupt(); - } - } - - public void receiveLoop() throws IOException - { - byte[] msg = new byte[35000]; - - while (true) - { - int msglen; - try - { - msglen = tc.receiveMessage(msg, 0, msg.length); - } - catch (SocketTimeoutException e) - { - // Timeout in read - if (idle) - { - log.debug("Ignoring socket timeout"); - continue; - } - throw e; - } - idle = true; - - int type = msg[0] & 0xff; - - if (type == Packets.SSH_MSG_IGNORE) - { - continue; - } - - if (type == Packets.SSH_MSG_DEBUG) - { - if (log.isDebugEnabled()) - { - TypesReader tr = new TypesReader(msg, 0, msglen); - tr.readByte(); - tr.readBoolean(); - StringBuilder debugMessageBuffer = new StringBuilder(); - debugMessageBuffer.append(tr.readString("UTF-8")); - - for (int i = 0; i < debugMessageBuffer.length(); i++) - { - char c = debugMessageBuffer.charAt(i); - - if ((c >= 32) && (c <= 126)) - { - continue; - } - debugMessageBuffer.setCharAt(i, '\uFFFD'); - } - - log.debug("DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); - } - continue; - } - - if (type == Packets.SSH_MSG_UNIMPLEMENTED) - { - throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); - } - - if (type == Packets.SSH_MSG_DISCONNECT) - { - TypesReader tr = new TypesReader(msg, 0, msglen); - tr.readByte(); - int reason_code = tr.readUINT32(); - StringBuilder reasonBuffer = new StringBuilder(); - reasonBuffer.append(tr.readString("UTF-8")); - - /* - * Do not get fooled by servers that send abnormal long error - * messages - */ - - if (reasonBuffer.length() > 255) - { - reasonBuffer.setLength(255); - reasonBuffer.setCharAt(254, '.'); - reasonBuffer.setCharAt(253, '.'); - reasonBuffer.setCharAt(252, '.'); - } - - /* - * Also, check that the server did not send characters that may - * screw up the receiver -> restrict to reasonable US-ASCII - * subset -> "printable characters" (ASCII 32 - 126). Replace - * all others with 0xFFFD (UNICODE replacement character). - */ - - for (int i = 0; i < reasonBuffer.length(); i++) - { - char c = reasonBuffer.charAt(i); - - if ((c >= 32) && (c <= 126)) - { - continue; - } - reasonBuffer.setCharAt(i, '\uFFFD'); - } - - throw new IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): " - + reasonBuffer.toString()); - } - - /* - * Is it a KEX Packet? - */ - - if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS) - || ((type >= 30) && (type <= 49))) - { - km.handleMessage(msg, msglen); - continue; - } - - MessageHandler mh = null; - - for (int i = 0; i < messageHandlers.size(); i++) - { - HandlerEntry he = messageHandlers.get(i); - if ((he.low <= type) && (type <= he.high)) - { - mh = he.mh; - break; - } - } - - if (mh == null) - { - throw new IOException("Unexpected SSH message (type " + type + ")"); - } - - mh.handleMessage(msg, msglen); - } - } -}