diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6b137f68ac..da97e99e08 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -124,6 +124,9 @@
+
+
+
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 2d31baad9b..f2195fa5c7 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -11,6 +11,7 @@ changes to this list.
* Andras Slemmer (R3)
* Andrius Dagys (R3)
* Andrzej Cichocki (R3)
+* Andrzej Grzesik (R3)
* Anthony Coates (Deutsche Bank)
* Anton Semenov (Commerzbank)
* Antonio Cerrato (SEB)
@@ -92,7 +93,7 @@ changes to this list.
* Matthijs van den Bos (ING)
* Michal Kit (R3)
* Micheal Hinstridge (Thoughtworks)
-* Michelle Sollecito (R3)
+* Michele Sollecito (R3)
* Mike Hearn (R3)
* Mike Reichelt (US Bank)
* Mustafa Ozturk (Natixis)
diff --git a/build.gradle b/build.gradle
index 473e047253..a78020efbb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,6 +73,7 @@ buildscript {
ext.selenium_version = '3.8.1'
ext.ghostdriver_version = '2.1.0'
ext.eaagentloader_version = '1.0.3'
+ ext.jsch_version = '0.1.54'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
index ef1eb579f2..7368f5fd88 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
@@ -73,7 +73,8 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration)
class CordaRPCClient private constructor(
hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
- sslConfiguration: SSLConfiguration? = null
+ sslConfiguration: SSLConfiguration? = null,
+ classLoader: ClassLoader? = null
) {
@JvmOverloads
constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(hostAndPort, configuration, null)
@@ -86,6 +87,15 @@ class CordaRPCClient private constructor(
): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, sslConfiguration)
}
+
+ internal fun createWithSslAndClassLoader(
+ hostAndPort: NetworkHostAndPort,
+ configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
+ sslConfiguration: SSLConfiguration? = null,
+ classLoader: ClassLoader? = null
+ ): CordaRPCClient {
+ return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader)
+ }
}
init {
@@ -93,7 +103,7 @@ class CordaRPCClient private constructor(
effectiveSerializationEnv
} catch (e: IllegalStateException) {
try {
- KryoClientSerializationScheme.initialiseSerialization()
+ KryoClientSerializationScheme.initialiseSerialization(classLoader)
} catch (e: IllegalStateException) {
// Race e.g. two of these constructed in parallel, ignore.
}
@@ -103,7 +113,7 @@ class CordaRPCClient private constructor(
private val rpcClient = RPCClient(
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration),
configuration.toRpcClientConfiguration(),
- KRYO_RPC_CLIENT_CONTEXT
+ if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT
)
/**
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt
index d5787f5dec..7526921453 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt
@@ -10,4 +10,11 @@ fun createCordaRPCClientWithSsl(
hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
sslConfiguration: SSLConfiguration? = null
-) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration)
\ No newline at end of file
+) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration)
+
+fun createCordaRPCClientWithSslAndClassLoader(
+ hostAndPort: NetworkHostAndPort,
+ configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
+ sslConfiguration: SSLConfiguration? = null,
+ classLoader: ClassLoader? = null
+) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
\ No newline at end of file
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt
index 6132509e21..998ac3c927 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt
@@ -33,18 +33,19 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
companion object {
/** Call from main only. */
- fun initialiseSerialization() {
- nodeSerializationEnv = createSerializationEnv()
+ fun initialiseSerialization(classLoader: ClassLoader? = null) {
+ nodeSerializationEnv = createSerializationEnv(classLoader)
}
- fun createSerializationEnv(): SerializationEnvironment {
+ fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment {
return SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme(emptyList()))
},
- AMQP_P2P_CONTEXT,
- rpcClientContext = KRYO_RPC_CLIENT_CONTEXT)
+ if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT,
+ rpcClientContext = if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT)
+
}
}
}
\ No newline at end of file
diff --git a/docs/source/shell.rst b/docs/source/shell.rst
index 90edacf5ff..d560467f7e 100644
--- a/docs/source/shell.rst
+++ b/docs/source/shell.rst
@@ -9,7 +9,7 @@ Shell
.. contents::
-The Corda shell is an embedded command line that allows an administrator to control and monitor a node. It is based on
+The Corda shell is an embedded or standalone command line that allows an administrator to control and monitor a node. It is based on
the `CRaSH`_ shell and supports many of the same features. These features include:
* Invoking any of the node's RPC methods
@@ -19,11 +19,22 @@ the `CRaSH`_ shell and supports many of the same features. These features includ
* Viewing JMX metrics and monitoring exports
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data
+Permissions
+-----------
+
+When accessing the shell (embedded, standalone, via SSH) RPC permissions are required. This is because the shell actually communicates
+with the node using RPC calls.
+
+* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``
+* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic``, ``InvokeRpc.registeredFlows`` and ``InvokeRpc.wellKnownPartyFromX500Name``, as well as a
+ permission for the flow being started
+
The shell via the local terminal
--------------------------------
-In development mode, the shell will display in the node's terminal window. It may be disabled by passing the
-``--no-local-shell`` flag when running the node.
+In development mode, the shell will display in the node's terminal window.
+The shell connects to the node as 'shell' user with password 'shell' which is only available in dev mode.
+It may be disabled by passing the ``--no-local-shell`` flag when running the node.
The shell via SSH
-----------------
@@ -42,8 +53,8 @@ By default, the SSH server is *disabled*. To enable it, a port must be configure
Authentication
**************
-Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates
-with the node using RPC calls. No RPC permissions are required to allow the connection and log in.
+Users log in to shell via SSH using the same credentials as for RPC.
+No RPC permissions are required to allow the connection and log in.
The host key is loaded from the ``/sshkey/hostkey.pem`` file. If this file does not exist, it is
generated automatically. In development mode, the seed may be specified to give the same results on the same computer
@@ -69,7 +80,7 @@ Where:
The RPC password will be requested after a connection is established.
-:note: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves
+.. note:: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves
trusted hosts and will refuse to connect in case of a change. This check can be disabled using the
``-o StrictHostKeyChecking=no`` flag. This option should never be used in production environment!
@@ -78,14 +89,99 @@ Windows
Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used.
-Permissions
-***********
+The standalone shell
+------------------------------
+The standalone shell is a standalone application interacting with a Corda node via RPC calls.
+RPC node permissions are necessary for authentication and authorisation.
+Certain operations, such as starting flows, require access to CordApps jars.
-When accessing the shell via SSH, some additional RPC permissions are required:
+Starting the standalone shell
+*************************
+
+Run the following command from the terminal:
+
+Linux and MacOS
+^^^^^^^^^^^^^^^
+
+.. code:: bash
+
+ ./shell [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT
+ --user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD
+ --keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help]
+
+Windows
+^^^^^^^
+
+.. code:: bash
+
+ shell.bat [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT
+ --user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD
+ --keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help]
+
+Where:
+
+* ``config-file`` is the path to config file, used instead of providing the rest of command line options
+* ``cordpass-directory`` is the directory containing Cordapps jars, Cordapps are require when starting flows
+* ``commands-directory`` is the directory with additional CrAsH shell commands
+* ``host`` is the Corda node's host
+* ``port`` is the Corda node's port, specified in the ``node.conf`` file
+* ``user`` is the RPC username, if not provided it will be requested at startup
+* ``password`` is the RPC user password, if not provided it will be requested at startup
+* ``sshd-port`` instructs the standalone shell app to start SSH server on the given port, optional
+* ``sshd-hostkey-directory`` is the directory containing hostkey.pem file for SSH server
+* ``keystore-password`` the password to unlock the KeyStore file containing the standalone shell certificate and private key, optional, unencrypted RPC connection without SSL will be used if the option is not provided
+* ``keystore-file`` is the path to the KeyStore file
+* ``truststore-password`` the password to unlock the TrustStore file containing the Corda node certificate, optional, unencrypted RPC connection without SSL will be used if the option is not provided
+* ``truststore-file`` is the path to the TrustStore file
+* ``help`` prints Shell help
+
+The format of ``config-file``:
+
+.. code:: bash
+
+ node {
+ addresses {
+ rpc {
+ host : "localhost"
+ port : 10006
+ }
+ }
+ }
+ shell {
+ workDir : /path/to/dir
+ }
+ extensions {
+ cordapps {
+ path : /path/to/cordapps/dir
+ }
+ sshd {
+ enabled : "false"
+ port : 2223
+ }
+ }
+ ssl {
+ keystore {
+ path: "/path/to/keystore"
+ type: "JKS"
+ password: password
+ }
+ trustore {
+ path: "/path/to/trusttore"
+ type: "JKS"
+ password: password
+ }
+ }
+ user : demo
+ password : demo
+
+
+Standalone Shell via SSH
+------------------------------------------
+The standalone shell can embed an SSH server which redirects interactions via RPC calls to the Corda node.
+To run SSH server use ``--sshd-port`` option when starting standalone shell or ``extensions.sshd`` entry in the configuration file.
+For connection to SSH refer to `Connecting to the shell`_.
+Certain operations (like starting Flows) will require Shell's ``--cordpass-directory`` to be configured correctly (see `Starting the standalone shell`_).
-* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``
-* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows``, as well as a
- permission for the flow being started
Interacting with the node via the shell
---------------------------------------
diff --git a/node/build.gradle b/node/build.gradle
index be7f3ee027..32fd8b6a10 100644
--- a/node/build.gradle
+++ b/node/build.gradle
@@ -65,6 +65,7 @@ dependencies {
compile project(':node-api')
compile project(":confidential-identities")
compile project(':client:rpc')
+ compile project(':tools:shell')
compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// Log4J: logging framework (with SLF4J bindings)
@@ -102,10 +103,6 @@ dependencies {
exclude group: "asm"
}
- // Jackson support: serialisation to/from JSON, YAML, etc
- compile project(':client:jackson')
- compile group: 'org.json', name: 'json', version: json_version
-
// Coda Hale's Metrics: for monitoring of key statistics
compile "io.dropwizard.metrics:metrics-core:3.1.2"
@@ -150,17 +147,6 @@ dependencies {
// Netty: All of it.
compile "io.netty:netty-all:$netty_version"
- // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy.
- compile("com.github.corda.crash:crash.shell:$crash_version") {
- exclude group: "org.slf4j", module: "slf4j-jdk14"
- exclude group: "org.bouncycastle"
- }
-
- compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") {
- exclude group: "org.slf4j", module: "slf4j-jdk14"
- exclude group: "org.bouncycastle"
- }
-
// OkHTTP: Simple HTTP library.
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
@@ -175,9 +161,6 @@ dependencies {
integrationTestCompile "junit:junit:$junit_version"
integrationTestCompile "org.assertj:assertj-core:${assertj_version}"
- // Jsh: Testing SSH server
- integrationTestCompile group: 'com.jcraft', name: 'jsch', version: '0.1.54'
-
// AgentLoader: dynamic loading of JVM agents
compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}"
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt
index 253f49aa0e..8252ef114c 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt
@@ -5,8 +5,8 @@ import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions.Companion.all
-import net.corda.node.testsupport.withCertificates
-import net.corda.node.testsupport.withKeyStores
+import net.corda.testing.common.internal.withCertificates
+import net.corda.testing.common.internal.withKeyStores
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
index 3ea2500904..eb2b936b5e 100644
--- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
@@ -42,6 +42,7 @@ import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.api.*
import net.corda.node.services.config.*
+import net.corda.node.services.config.shell.toShellConfig
import net.corda.node.services.events.NodeSchedulerService
import net.corda.node.services.events.ScheduledActivityObserver
import net.corda.node.services.identity.PersistentIdentityService
@@ -56,7 +57,6 @@ import net.corda.node.services.transactions.*
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSoftLockManager
-import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.JVMAgentRegistry
import net.corda.node.utilities.NodeBuildProperties
@@ -67,6 +67,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.storeLegalIdentity
+import net.corda.tools.shell.InteractiveShell
import org.apache.activemq.artemis.utils.ReusableLatch
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
import org.slf4j.Logger
@@ -258,7 +259,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
tokenizableServices = nodeServices + cordaServices + schedulerService
registerCordappFlows(smm)
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
- startShell(rpcOps)
+ startShell()
Pair(StartedNodeImpl(this@AbstractNode, _services, nodeInfo, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
}
networkMapUpdater = NetworkMapUpdater(services.networkMapCache,
@@ -296,9 +297,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
*/
protected abstract fun getRxIoScheduler(): Scheduler
- open fun startShell(rpcOps: CordaRPCOps) {
+ open fun startShell() {
if (configuration.shouldInitCrashShell()) {
- InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database)
+ if (configuration.rpcOptions.address == null) {
+ throw ConfigurationException("Cannot init CrashShell because node RPC address is not set (via 'rpcSettings' option).")
+ }
+ InteractiveShell.startShell(configuration.toShellConfig())
}
}
diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt
index 6a3b71534f..59bd348cd1 100644
--- a/node/src/main/kotlin/net/corda/node/internal/Node.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt
@@ -1,6 +1,7 @@
package net.corda.node.internal
import com.codahale.metrics.JmxReporter
+import net.corda.client.rpc.internal.KryoClientSerializationScheme
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.thenMatch
@@ -26,9 +27,8 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.api.NodePropertiesStore
import net.corda.node.services.api.SchemaService
-import net.corda.node.services.config.NodeConfiguration
-import net.corda.node.services.config.SecurityConfiguration
-import net.corda.node.services.config.VerifierType
+import net.corda.node.services.config.*
+import net.corda.node.services.config.shell.shellUser
import net.corda.node.services.messaging.*
import net.corda.node.services.rpc.ArtemisRpcBroker
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
@@ -159,7 +159,7 @@ open class Node(configuration: NodeConfiguration,
val securityManagerConfig = configuration.security?.authService ?:
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
- securityManager = RPCSecurityManagerImpl(securityManagerConfig)
+ securityManager = RPCSecurityManagerImpl(if (configuration.shouldInitCrashShell()) securityManagerConfig.copyWithAdditionalUser(configuration.shellUser()) else securityManagerConfig)
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker(networkParameters)
val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) {
@@ -373,11 +373,13 @@ open class Node(configuration: NodeConfiguration,
SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps))
+ registerScheme(KryoClientSerializationScheme())
},
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
rpcServerContext = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader),
storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
- checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader))
+ checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader),
+ rpcClientContext = if (configuration.shouldInitCrashShell()) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node
}
private var rpcMessagingClient: RPCMessagingClient? = null
diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
index af04a57afd..be082e1023 100644
--- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
@@ -12,12 +12,13 @@ import net.corda.node.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.shouldStartLocalShell
+import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.services.transactions.bftSMaRtSerialFilter
-import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
+import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler
@@ -153,12 +154,15 @@ open class NodeStartup(val args: Array) {
if (conf.shouldStartLocalShell()) {
startedNode.internals.startupComplete.then {
try {
- InteractiveShell.runLocalShell(startedNode)
+ InteractiveShell.runLocalShell( {startedNode.dispose()} )
} catch (e: Throwable) {
logger.error("Shell failed to start", e)
}
}
}
+ if (conf.shouldStartSSHDaemon()) {
+ Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
+ }
},
{ th ->
logger.error("Unexpected exception during registration", th)
diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
index 1e3807a1e7..858b243aeb 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
@@ -14,6 +14,7 @@ import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.persistence.DatabaseConfig
+import net.corda.tools.shell.SSHDConfiguration
import java.net.URL
import java.nio.file.Path
import java.time.Duration
@@ -253,8 +254,6 @@ data class CertChainPolicyConfig(val role: String, private val policy: CertChain
}
}
-data class SSHDConfiguration(val port: Int)
-
// Supported types of authentication/authorization data providers
enum class AuthDataSourceType {
// External RDBMS
@@ -290,6 +289,8 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
}
}
+ fun copyWithAdditionalUser(user: User) = AuthService(dataSource.copyWithAdditionalUser(user), id, options)
+
// Optional components: cache
data class Options(val cache: Options.Cache?) {
@@ -317,6 +318,12 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
AuthDataSourceType.DB -> require(users == null && connection != null)
}
}
+
+ fun copyWithAdditionalUser(user: User) : DataSource{
+ val extendedList = this.users?.toMutableList()?: mutableListOf()
+ extendedList.add(user)
+ return DataSource(this.type, this.passwordEncryption, this.connection, listOf(*extendedList.toTypedArray()))
+ }
}
companion object {
diff --git a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt b/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt
index e4b6f7f9f5..6f1fd1941e 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt
@@ -5,7 +5,6 @@ import java.nio.file.Path
import java.nio.file.Paths
data class SslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration {
- constructor(certificatesDirectory: String, keyStorePassword: String, trustStorePassword: String) : this(certificatesDirectory.toAbsolutePath(), keyStorePassword, trustStorePassword)
fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(), keyStorePassword: String = this.keyStorePassword, trustStorePassword: String = this.trustStorePassword): SslOptions = copy(certificatesDirectory = certificatesDirectory.toAbsolutePath(), keyStorePassword = keyStorePassword, trustStorePassword = trustStorePassword)
}
diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt
new file mode 100644
index 0000000000..00fa4e0f81
--- /dev/null
+++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt
@@ -0,0 +1,44 @@
+package net.corda.node.services.config.shell
+
+import net.corda.core.internal.div
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.node.services.Permissions
+import net.corda.node.services.config.NodeConfiguration
+import net.corda.node.services.config.shouldInitCrashShell
+import net.corda.nodeapi.internal.config.User
+import net.corda.tools.shell.ShellConfiguration
+import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
+import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
+import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR
+import net.corda.tools.shell.ShellConfiguration.Companion.SSH_PORT
+import net.corda.tools.shell.ShellSslOptions
+
+
+//re-packs data to Shell specific classes
+fun NodeConfiguration.toShellConfig(): ShellConfiguration {
+
+ val sslConfiguration = if (this.rpcOptions.useSsl) {
+ with(this.rpcOptions.sslConfig) {
+ ShellSslOptions(sslKeystore,
+ keyStorePassword,
+ trustStoreFile,
+ trustStorePassword)
+ }
+ } else {
+ null
+ }
+ val localShellUser: User = localShellUser()
+ return ShellConfiguration(
+ commandsDirectory = this.baseDirectory / COMMANDS_DIR,
+ cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
+ user = localShellUser.username,
+ password = localShellUser.password,
+ hostAndPort = this.rpcOptions.address ?: NetworkHostAndPort("localhost", SSH_PORT),
+ ssl = sslConfiguration,
+ sshdPort = this.sshd?.port,
+ sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
+ noLocalShell = this.noLocalShell)
+}
+
+private fun localShellUser() = User("shell", "shell", setOf(Permissions.all()))
+fun NodeConfiguration.shellUser() = shouldInitCrashShell()?.let { localShellUser() }
diff --git a/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt b/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt
deleted file mode 100644
index 7dbdc8e52f..0000000000
--- a/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.corda.node.shell
-
-import net.corda.core.context.Actor
-import net.corda.core.context.InvocationContext
-import net.corda.core.identity.CordaX500Name
-import net.corda.core.messaging.CordaRPCOps
-import net.corda.node.internal.security.Password
-import net.corda.node.internal.security.RPCSecurityManager
-import net.corda.node.internal.security.tryAuthenticate
-import org.crsh.auth.AuthInfo
-import org.crsh.auth.AuthenticationPlugin
-import org.crsh.plugin.CRaSHPlugin
-
-class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin>(), AuthenticationPlugin {
-
- override fun getImplementation(): AuthenticationPlugin = this
-
- override fun getName(): String = "corda"
-
- override fun authenticate(username: String?, credential: String?): AuthInfo {
-
- if (username == null || credential == null) {
- return AuthInfo.UNSUCCESSFUL
- }
- val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential))
- if (authorizingSubject != null) {
- val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName)
- return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject))
- }
- return AuthInfo.UNSUCCESSFUL
- }
-
- override fun getCredentialType(): Class = String::class.java
-}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt b/node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt
deleted file mode 100644
index 04bda1a4bb..0000000000
--- a/node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.corda.node.shell
-
-import net.corda.core.messaging.CordaRPCOps
-import net.corda.node.utilities.ANSIProgressRenderer
-import org.crsh.auth.AuthInfo
-
-class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null) : AuthInfo {
- override fun isSuccessful(): Boolean = successful
-}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt b/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt
deleted file mode 100644
index 01446bd58d..0000000000
--- a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.corda.node.shell
-
-import net.corda.core.context.InvocationContext
-import net.corda.core.messaging.CordaRPCOps
-import net.corda.core.utilities.getOrThrow
-import net.corda.node.internal.security.AuthorizingSubject
-import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
-import net.corda.node.services.messaging.RpcAuthContext
-import java.lang.reflect.InvocationTargetException
-import java.lang.reflect.Proxy
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Future
-
-fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, authorizingSubject: AuthorizingSubject) : CordaRPCOps {
-
- return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { _, method, args ->
- RPCContextRunner(invocationContext, authorizingSubject) {
- try {
- method.invoke(cordaRPCOps, *(args ?: arrayOf()))
- } catch (e: InvocationTargetException) {
- // Unpack exception.
- throw e.targetException
- }
- }.get().getOrThrow()
- }) as CordaRPCOps
-}
-
-private class RPCContextRunner(val invocationContext: InvocationContext, val authorizingSubject: AuthorizingSubject, val block:() -> T): Thread() {
-
- private var result: CompletableFuture = CompletableFuture()
-
- override fun run() {
- CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, authorizingSubject))
- try {
- result.complete(block())
- } catch (e: Throwable) {
- result.completeExceptionally(e)
- } finally {
- CURRENT_RPC_CONTEXT.remove()
- }
- }
-
- fun get(): Future {
- start()
- join()
- return result
- }
-}
\ No newline at end of file
diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
index 21899cce86..394ef4a303 100644
--- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
@@ -2,6 +2,7 @@ package net.corda.node.services.config
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.tools.shell.SSHDConfiguration
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThatThrownBy
diff --git a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt
index 47a31bf0de..d065b0d222 100644
--- a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt
@@ -11,12 +11,12 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.Permissions.Companion.all
import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.messaging.RPCMessagingClient
-import net.corda.node.testsupport.withCertificates
-import net.corda.node.testsupport.withKeyStores
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User
+import net.corda.testing.common.internal.withCertificates
+import net.corda.testing.common.internal.withKeyStores
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.internal.RandomFree
diff --git a/settings.gradle b/settings.gradle
index 1191e87414..84cde047f2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -35,6 +35,7 @@ include 'tools:demobench'
include 'tools:loadtest'
include 'tools:graphs'
include 'tools:bootstrapper'
+include 'tools:shell'
include 'example-code'
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
include 'samples:attachment-demo'
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt
index 57b8e47b19..491c8a8dca 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt
@@ -278,7 +278,7 @@ open class InternalMockNetwork(private val cordappPackages: List,
return E2ETestKeyManagementService(identityService, keyPairs)
}
- override fun startShell(rpcOps: CordaRPCOps) {
+ override fun startShell() {
//No mock shell
}
diff --git a/node/src/test/kotlin/net/corda/node/testsupport/UnsafeCertificatesFactory.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt
similarity index 94%
rename from node/src/test/kotlin/net/corda/node/testsupport/UnsafeCertificatesFactory.kt
rename to testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt
index bac182604e..bb35d341f1 100644
--- a/node/src/test/kotlin/net/corda/node/testsupport/UnsafeCertificatesFactory.kt
+++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt
@@ -1,8 +1,8 @@
-package net.corda.node.testsupport
+package net.corda.testing.common.internal
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
-import net.corda.node.services.config.SslOptions
+import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.*
import org.apache.commons.io.FileUtils
import sun.security.tools.keytool.CertAndKeyGen
@@ -74,12 +74,13 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) {
}
}
}
+ data class TestSslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration
- private fun sslConfiguration(directory: Path) = SslOptions(directory, keyStore.password, trustStore.password)
+ private fun sslConfiguration(directory: Path) = TestSslOptions(directory, keyStore.password, trustStore.password)
}
interface AutoClosableSSLConfiguration : AutoCloseable {
- val value: SslOptions
+ val value: SSLConfiguration
}
typealias KeyStoreEntry = Pair
@@ -182,7 +183,7 @@ private fun newKeyStore(type: String, password: String): KeyStore {
return keyStore
}
-fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: SslOptions, clientSslOptions: SslOptions) -> Unit) {
+fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: SSLConfiguration, clientSslOptions: SSLConfiguration) -> Unit) {
val serverDir = Files.createTempDirectory(null)
FileUtils.forceDeleteOnExit(serverDir.toFile())
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
index fd064cc6cf..72e4b5a9ee 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
@@ -9,7 +9,6 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
-import net.corda.node.services.config.SslOptions
import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevNodeCa
@@ -121,7 +120,7 @@ fun createDevNodeCaCertPath(
/** Application of [doAnswer] that gets a value from the given [map] using the arg at [argIndex] as key. */
fun doLookup(map: Map<*, *>, argIndex: Int = 0) = doAnswer { map[it.arguments[argIndex]] }
-fun SslOptions.useSslRpcOverrides(): Map {
+fun SSLConfiguration.useSslRpcOverrides(): Map {
return mapOf(
"rpcSettings.useSsl" to "true",
"rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(),
@@ -130,7 +129,7 @@ fun SslOptions.useSslRpcOverrides(): Map {
)
}
-fun SslOptions.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map {
+fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map {
return mapOf(
"rpcSettings.adminAddress" to rpcAdminAddress.toString(),
"rpcSettings.useSsl" to "false",
diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle
new file mode 100644
index 0000000000..73c5aafb16
--- /dev/null
+++ b/tools/shell/build.gradle
@@ -0,0 +1,92 @@
+apply plugin: 'kotlin'
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'net.corda.plugins.quasar-utils'
+
+description 'Corda Shell'
+
+configurations {
+ integrationTestCompile.extendsFrom testCompile
+ integrationTestRuntime.extendsFrom testRuntime
+}
+
+sourceSets {
+ integrationTest {
+ kotlin {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ srcDir file('src/integration-test/kotlin')
+ }
+ resources {
+ srcDir file('src/integration-test/resources')
+ }
+ }
+ test {
+ resources {
+ srcDir file('src/test/resources')
+ }
+ }
+}
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
+
+ compile project(':node-api')
+ compile project(':client:rpc')
+
+ // Jackson support: serialisation to/from JSON, YAML, etc
+ compile project(':client:jackson')
+ compile group: 'org.json', name: 'json', version: json_version
+
+
+ // JOpt: for command line flags.
+ compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
+
+ // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy.
+ compile("com.github.corda.crash:crash.shell:$crash_version") {
+ exclude group: "org.slf4j", module: "slf4j-jdk14"
+ exclude group: "org.bouncycastle"
+ }
+
+ compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") {
+ exclude group: "org.slf4j", module: "slf4j-jdk14"
+ exclude group: "org.bouncycastle"
+ }
+
+ // JAnsi: for drawing things to the terminal in nicely coloured ways.
+ compile "org.fusesource.jansi:jansi:$jansi_version"
+
+ // Manifests: for reading stuff from the manifest file
+ compile "com.jcabi:jcabi-manifests:1.1"
+
+ // Unit testing helpers.
+ testCompile "junit:junit:$junit_version"
+ testCompile "org.assertj:assertj-core:${assertj_version}"
+ testCompile project(':test-utils')
+ testCompile project(':finance')
+
+ // Integration test helpers
+ integrationTestCompile "junit:junit:$junit_version"
+ integrationTestCompile "org.assertj:assertj-core:${assertj_version}"
+
+ // Jsh: Testing SSH server
+ integrationTestCompile "com.jcraft:jsch:$jsch_version"
+
+ integrationTestCompile project(':node-driver')
+}
+
+mainClassName = 'net.corda.tools.shell.StandaloneShellKt'
+
+jar {
+ baseName 'corda-shell'
+}
+
+processResources {
+ from file("$rootDir/config/dev/log4j2.xml")
+}
+
+task integrationTest(type: Test) {
+ testClassesDirs = sourceSets.integrationTest.output.classesDirs
+ classpath = sourceSets.integrationTest.runtimeClasspath
+}
diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt
new file mode 100644
index 0000000000..37b9be0e04
--- /dev/null
+++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt
@@ -0,0 +1,239 @@
+package net.corda.tools.shell
+
+import com.google.common.io.Files
+import com.jcraft.jsch.ChannelExec
+import com.jcraft.jsch.JSch
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.messaging.CordaRPCOps
+import net.corda.core.utilities.getOrThrow
+import net.corda.node.services.Permissions
+import net.corda.node.services.Permissions.Companion.all
+import net.corda.testing.common.internal.withCertificates
+import net.corda.testing.common.internal.withKeyStores
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.driver.DriverParameters
+import net.corda.testing.driver.driver
+import net.corda.testing.driver.internal.RandomFree
+import net.corda.testing.internal.useSslRpcOverrides
+import net.corda.testing.node.User
+import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
+import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.bouncycastle.util.io.Streams
+import org.junit.Test
+import kotlin.test.assertTrue
+
+class InteractiveShellIntegrationTest {
+
+ @Test
+ fun `shell should not log in with invalid credentials`() {
+ val user = User("u", "p", setOf())
+ driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
+ val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
+ val node = nodeFuture.getOrThrow()
+
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = "fake", password = "fake",
+ hostAndPort = node.rpcAddress)
+ InteractiveShell.startShell(conf)
+
+ assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
+ }
+ }
+
+ @Test
+ fun `shell should log in with valid crentials`() {
+ val user = User("u", "p", setOf())
+ driver {
+ val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
+ val node = nodeFuture.getOrThrow()
+
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = user.username, password = user.password,
+ hostAndPort = node.rpcAddress)
+
+ InteractiveShell.startShell(conf)
+ InteractiveShell.nodeInfo()
+ }
+ }
+
+ @Test
+ fun `shell should log in with ssl`() {
+ val user = User("mark", "dadada", setOf(all()))
+ withCertificates { server, client, createSelfSigned, createSignedBy ->
+ val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
+ val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
+
+ // truststore needs to contain root CA for how the driver works...
+ server.keyStore["cordaclienttls"] = rootCertificate
+ server.trustStore["cordaclienttls"] = rootCertificate
+ server.trustStore["shell"] = markCertificate
+
+ client.keyStore["shell"] = markCertificate
+ client.trustStore["cordaclienttls"] = rootCertificate
+
+ withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
+ var successful = false
+ driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
+ startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
+
+ val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
+ clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = user.username, password = user.password,
+ hostAndPort = node.rpcAddress,
+ ssl = sslConfiguration)
+
+ InteractiveShell.startShell(conf)
+
+ InteractiveShell.nodeInfo()
+ successful = true
+ }
+ }
+ assertThat(successful).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun `shell shoud not log in without ssl keystore`() {
+ val user = User("mark", "dadada", setOf("ALL"))
+ withCertificates { server, client, createSelfSigned, createSignedBy ->
+ val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
+ val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
+
+ // truststore needs to contain root CA for how the driver works...
+ server.keyStore["cordaclienttls"] = rootCertificate
+ server.trustStore["cordaclienttls"] = rootCertificate
+ server.trustStore["shell"] = markCertificate
+
+ //client key store doesn't have "mark" certificate
+ client.trustStore["cordaclienttls"] = rootCertificate
+
+ withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
+ driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
+ startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
+
+ val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
+ clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = user.username, password = user.password,
+ hostAndPort = node.rpcAddress,
+ ssl = sslConfiguration)
+
+ InteractiveShell.startShell(conf)
+
+ assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `ssh runs flows via standalone shell`() {
+ val user = User("u", "p", setOf(Permissions.startFlow(),
+ Permissions.invokeRpc(CordaRPCOps::registeredFlows),
+ Permissions.invokeRpc(CordaRPCOps::nodeInfo)))
+ driver {
+ val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
+ val node = nodeFuture.getOrThrow()
+
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = user.username, password = user.password,
+ hostAndPort = node.rpcAddress,
+ sshdPort = 2224)
+
+ InteractiveShell.startShell(conf)
+ InteractiveShell.nodeInfo()
+
+ val session = JSch().getSession("u", "localhost", 2224)
+ session.setConfig("StrictHostKeyChecking", "no")
+ session.setPassword("p")
+ session.connect()
+
+ assertTrue(session.isConnected)
+
+ val channel = session.openChannel("exec") as ChannelExec
+ channel.setCommand("start FlowICanRun")
+ channel.connect(5000)
+
+ assertTrue(channel.isConnected)
+
+ val response = String(Streams.readAll(channel.inputStream))
+
+ val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
+
+ channel.disconnect()
+ session.disconnect()
+
+ // There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
+ assertThat(linesWithDoneCount).hasSize(1)
+ }
+ }
+
+ @Test
+ fun `ssh run flows via standalone shell over ssl to node`() {
+ val user = User("mark", "dadada", setOf(Permissions.startFlow(),
+ Permissions.invokeRpc(CordaRPCOps::registeredFlows),
+ Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/))
+ withCertificates { server, client, createSelfSigned, createSignedBy ->
+ val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
+ val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
+
+ // truststore needs to contain root CA for how the driver works...
+ server.keyStore["cordaclienttls"] = rootCertificate
+ server.trustStore["cordaclienttls"] = rootCertificate
+ server.trustStore["shell"] = markCertificate
+
+ client.keyStore["shell"] = markCertificate
+ client.trustStore["cordaclienttls"] = rootCertificate
+
+ withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
+ var successful = false
+ driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
+ startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
+
+ val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
+ clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
+ val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
+ user = user.username, password = user.password,
+ hostAndPort = node.rpcAddress,
+ ssl = sslConfiguration,
+ sshdPort = 2223)
+
+ InteractiveShell.startShell(conf)
+ InteractiveShell.nodeInfo()
+
+ val session = JSch().getSession("mark", "localhost", 2223)
+ session.setConfig("StrictHostKeyChecking", "no")
+ session.setPassword("dadada")
+ session.connect()
+
+ assertTrue(session.isConnected)
+
+ val channel = session.openChannel("exec") as ChannelExec
+ channel.setCommand("start FlowICanRun")
+ channel.connect(5000)
+
+ assertTrue(channel.isConnected)
+
+ val response = String(Streams.readAll(channel.inputStream))
+
+ val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
+
+ channel.disconnect()
+ session.disconnect() // TODO Simon make sure to close them
+
+ // There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
+ assertThat(linesWithDoneCount).hasSize(1)
+
+ successful = true
+ }
+ }
+ assertThat(successful).isTrue()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt
similarity index 94%
rename from node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt
rename to tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt
index 634b761d1f..7689bb89d9 100644
--- a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt
+++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt
@@ -1,4 +1,4 @@
-package net.corda.node
+package net.corda.tools.shell
import co.paralleluniverse.fibers.Suspendable
import com.jcraft.jsch.ChannelExec
@@ -8,9 +8,11 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
+import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
+import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
@@ -20,7 +22,6 @@ import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.util.io.Streams
import org.junit.Test
import java.net.ConnectException
-import java.util.regex.Pattern
import kotlin.test.assertTrue
import kotlin.test.fail
@@ -91,7 +92,8 @@ class SSHServerTest {
@Test
fun `ssh respects permissions`() {
- val user = User("u", "p", setOf(startFlow()))
+ val user = User("u", "p", setOf(startFlow(),
+ invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name)))
// The driver will automatically pick up the annotated flows below
driver(DriverParameters(isDebug = true)) {
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
@@ -106,12 +108,10 @@ class SSHServerTest {
assertTrue(session.isConnected)
val channel = session.openChannel("exec") as ChannelExec
- channel.setCommand("start FlowICannotRun otherParty: \"${ALICE_NAME}\"")
+ channel.setCommand("start FlowICannotRun otherParty: \"$ALICE_NAME\"")
channel.connect()
val response = String(Streams.readAll(channel.inputStream))
- val flowNameEscaped = Pattern.quote("StartFlow.${SSHServerTest::class.qualifiedName}$${FlowICannotRun::class.simpleName}")
-
channel.disconnect()
session.disconnect()
@@ -137,11 +137,17 @@ class SSHServerTest {
val channel = session.openChannel("exec") as ChannelExec
channel.setCommand("start FlowICanRun")
- channel.connect()
+ channel.connect(5000)
+
+ assertTrue(channel.isConnected)
val response = String(Streams.readAll(channel.inputStream))
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
+
+ channel.disconnect()
+ session.disconnect()
+
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
assertThat(linesWithDoneCount).hasSize(1)
}
diff --git a/tools/shell/src/integration-test/resources/ssl.conf b/tools/shell/src/integration-test/resources/ssl.conf
new file mode 100644
index 0000000000..f8faaa8788
--- /dev/null
+++ b/tools/shell/src/integration-test/resources/ssl.conf
@@ -0,0 +1,8 @@
+user=demo1
+baseDirectory="/Users/szymonsztuka/Documents/shell-config"
+hostAndPort="localhost:10006"
+sshdPort=2223
+ssl {
+ keyStorePassword=password
+ trustStorePassword=password
+}
diff --git a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java
similarity index 85%
rename from node/src/main/java/net/corda/node/shell/FlowShellCommand.java
rename to tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java
index f857a7eb62..1ed76de5f0 100644
--- a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java
+++ b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java
@@ -1,10 +1,11 @@
-package net.corda.node.shell;
+package net.corda.tools.shell;
// See the comments at the top of run.java
+import com.fasterxml.jackson.databind.ObjectMapper;
import net.corda.core.messaging.CordaRPCOps;
-import net.corda.node.utilities.ANSIProgressRenderer;
-import net.corda.node.utilities.CRaSHANSIProgressRenderer;
+import net.corda.tools.shell.utlities.ANSIProgressRenderer;
+import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer;
import org.crsh.cli.*;
import org.crsh.command.*;
import org.crsh.text.*;
@@ -12,7 +13,8 @@ import org.crsh.text.ui.TableElement;
import java.util.*;
-import static net.corda.node.shell.InteractiveShell.*;
+import static net.corda.tools.shell.InteractiveShell.runFlowByNameFragment;
+import static net.corda.tools.shell.InteractiveShell.runStateMachinesView;
@Man(
"Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" +
@@ -28,7 +30,7 @@ public class FlowShellCommand extends InteractiveShellCommand {
@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name,
@Usage("The data to pass as input") @Argument(unquote = false) List input
) {
- startFlow(name, input, out, ops(), ansiProgressRenderer());
+ startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper());
}
// TODO Limit number of flows shown option?
@@ -42,13 +44,14 @@ public class FlowShellCommand extends InteractiveShellCommand {
@Usage("The data to pass as input") @Argument(unquote = false) List input,
RenderPrintWriter out,
CordaRPCOps rpcOps,
- ANSIProgressRenderer ansiProgressRenderer) {
+ ANSIProgressRenderer ansiProgressRenderer,
+ ObjectMapper om) {
if (name == null) {
out.println("You must pass a name for the flow, see 'man flow'", Color.red);
return;
}
String inp = input == null ? "" : String.join(" ", input).trim();
- runFlowByNameFragment(name, inp, out, rpcOps, ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out) );
+ runFlowByNameFragment(name, inp, out, rpcOps, ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), om);
}
@Command
diff --git a/node/src/main/java/net/corda/node/shell/RunShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java
similarity index 97%
rename from node/src/main/java/net/corda/node/shell/RunShellCommand.java
rename to tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java
index 6875a5cdb8..c946190c05 100644
--- a/node/src/main/java/net/corda/node/shell/RunShellCommand.java
+++ b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java
@@ -1,4 +1,4 @@
-package net.corda.node.shell;
+package net.corda.tools.shell;
import net.corda.core.messaging.*;
import net.corda.client.jackson.*;
@@ -30,7 +30,7 @@ public class RunShellCommand extends InteractiveShellCommand {
return null;
}
- return InteractiveShell.runRPCFromString(command, out, context, ops());
+ return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper());
}
private void emitHelp(InvocationContext