From 8fe94bca2d2cdd971aec448ce91428218c0831d7 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 7 Mar 2018 09:55:41 +0000 Subject: [PATCH 1/7] ENT-1575 Shorten table name (#2750) --- .../corda/node/services/persistence/NodeAttachmentService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index cd9f7d37f5..9593cfc634 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -93,7 +93,7 @@ class NodeAttachmentService( @ElementCollection @Column(name = "contract_class_name") - @CollectionTable(name = "node_attachments_contract_class_name", joinColumns = arrayOf( + @CollectionTable(name = "node_attchments_contracts", joinColumns = arrayOf( JoinColumn(name = "att_id", referencedColumnName = "att_id")), foreignKey = ForeignKey(name = "FK__ctr_class__attachments")) var contractClassNames: List? = null From 72074c76c71fb49de258878604beb17da05f1a71 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 7 Mar 2018 09:57:32 +0000 Subject: [PATCH 2/7] [CORDA-792] Standalone Shell (#2663) - Existing embedded Shell connects via RPC including checking RPC user credentials (before was a direct use of CordaRPCOps): in dev mode when console terminal is enabled, node created `shell` user. - New Standalone Shell app with the same functionalities as Shell: connects to a node via RPC Client, can use SSL and run SSH server. --- .idea/compiler.xml | 3 + CONTRIBUTORS.md | 3 +- build.gradle | 1 + .../net/corda/client/rpc/CordaRPCClient.kt | 16 +- .../rpc/internal/CordaRPCClientUtils.kt | 9 +- .../internal/KryoClientSerializationScheme.kt | 11 +- docs/source/shell.rst | 120 ++++++++- node/build.gradle | 19 +- .../net/corda/node/services/rpc/RpcSslTest.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 12 +- .../kotlin/net/corda/node/internal/Node.kt | 12 +- .../net/corda/node/internal/NodeStartup.kt | 8 +- .../node/services/config/NodeConfiguration.kt | 11 +- .../corda/node/services/config/SslOptions.kt | 1 - .../node/services/config/shell/ShellConfig.kt | 44 ++++ .../node/shell/CordaAuthenticationPlugin.kt | 34 --- .../net/corda/node/shell/CordaSSHAuthInfo.kt | 9 - .../net/corda/node/shell/RPCOpsWithContext.kt | 48 ---- .../config/NodeConfigurationImplTest.kt | 1 + .../node/services/rpc/ArtemisRpcTests.kt | 4 +- settings.gradle | 1 + .../node/internal/InternalMockNetwork.kt | 2 +- .../internal}/UnsafeCertificatesFactory.kt | 11 +- .../testing/internal/InternalTestUtils.kt | 5 +- tools/shell/build.gradle | 92 +++++++ .../shell/InteractiveShellIntegrationTest.kt | 239 ++++++++++++++++++ .../net/corda/tools/shell}/SSHServerTest.kt | 20 +- .../src/integration-test/resources/ssl.conf | 8 + .../corda/tools}/shell/FlowShellCommand.java | 17 +- .../corda/tools}/shell/RunShellCommand.java | 4 +- .../corda/tools}/shell/StartShellCommand.java | 8 +- .../tools/shell/CordaAuthenticationPlugin.kt | 37 +++ .../net/corda/tools/shell/CordaSSHAuthInfo.kt | 15 ++ .../shell/FlowWatchPrintingSubscriber.kt | 2 +- .../corda/tools}/shell/InteractiveShell.kt | 184 ++++++++------ .../tools}/shell/InteractiveShellCommand.kt | 8 +- .../corda/tools/shell/RPCOpsWithContext.kt | 21 ++ .../net/corda/tools/shell/StandaloneShell.kt | 110 ++++++++ .../tools/shell/StandaloneShellArgsParser.kt | 226 +++++++++++++++++ .../shell/utlities}/ANSIProgressRenderer.kt | 2 +- .../net/corda/tools}/shell/base/login.groovy | 2 +- .../shell/CustomTypeJsonParsingTests.kt | 2 +- .../tools/shell}/InteractiveShellTest.kt | 24 +- .../shell/StandaloneShellArgsParserTest.kt | 204 +++++++++++++++ tools/shell/src/test/resources/config.conf | 34 +++ 45 files changed, 1367 insertions(+), 281 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt delete mode 100644 node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt delete mode 100644 node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt delete mode 100644 node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt rename {node/src/test/kotlin/net/corda/node/testsupport => testing/test-common/src/main/kotlin/net/corda/testing/common/internal}/UnsafeCertificatesFactory.kt (94%) create mode 100644 tools/shell/build.gradle create mode 100644 tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt rename {node/src/integration-test/kotlin/net/corda/node => tools/shell/src/integration-test/kotlin/net/corda/tools/shell}/SSHServerTest.kt (94%) create mode 100644 tools/shell/src/integration-test/resources/ssl.conf rename {node/src/main/java/net/corda/node => tools/shell/src/main/java/net/corda/tools}/shell/FlowShellCommand.java (85%) rename {node/src/main/java/net/corda/node => tools/shell/src/main/java/net/corda/tools}/shell/RunShellCommand.java (97%) rename {node/src/main/java/net/corda/node => tools/shell/src/main/java/net/corda/tools}/shell/StartShellCommand.java (78%) create mode 100644 tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt create mode 100644 tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt rename {node/src/main/kotlin/net/corda/node => tools/shell/src/main/kotlin/net/corda/tools}/shell/FlowWatchPrintingSubscriber.kt (99%) rename {node/src/main/kotlin/net/corda/node => tools/shell/src/main/kotlin/net/corda/tools}/shell/InteractiveShell.kt (81%) rename {node/src/main/kotlin/net/corda/node => tools/shell/src/main/kotlin/net/corda/tools}/shell/InteractiveShellCommand.kt (57%) create mode 100644 tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt create mode 100644 tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt create mode 100644 tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt rename {node/src/main/kotlin/net/corda/node/utilities => tools/shell/src/main/kotlin/net/corda/tools/shell/utlities}/ANSIProgressRenderer.kt (99%) rename {node/src/main/resources/net/corda/node => tools/shell/src/main/resources/net/corda/tools}/shell/base/login.groovy (90%) rename {node/src/test/kotlin/net/corda/node => tools/shell/src/test/kotlin/net/corda/tools}/shell/CustomTypeJsonParsingTests.kt (98%) rename {node/src/test/kotlin/net/corda/node => tools/shell/src/test/kotlin/net/corda/tools/shell}/InteractiveShellTest.kt (81%) create mode 100644 tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt create mode 100644 tools/shell/src/test/resources/config.conf 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 context, StringToMethodCallParser parser) { diff --git a/node/src/main/java/net/corda/node/shell/StartShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java similarity index 78% rename from node/src/main/java/net/corda/node/shell/StartShellCommand.java rename to tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java index 3d2b9953e9..2f368128c1 100644 --- a/node/src/main/java/net/corda/node/shell/StartShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java @@ -1,9 +1,9 @@ -package net.corda.node.shell; +package net.corda.tools.shell; // A simple forwarder to the "flow start" command, for easier typing. -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 java.util.*; @@ -14,6 +14,6 @@ public class StartShellCommand extends InteractiveShellCommand { public void main(@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) { ANSIProgressRenderer ansiProgressRenderer = ansiProgressRenderer(); - FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out)); + FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), objectMapper()); } } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt new file mode 100644 index 0000000000..c495292d0f --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt @@ -0,0 +1,37 @@ +package net.corda.tools.shell + +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.loggerFor +import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.crsh.auth.AuthInfo +import org.crsh.auth.AuthenticationPlugin +import org.crsh.plugin.CRaSHPlugin + +class CordaAuthenticationPlugin(private val rpcOps: (username: String, credential: String) -> CordaRPCOps): CRaSHPlugin>(), AuthenticationPlugin { + + companion object { + private val logger = loggerFor() + } + + 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 + } + try { + val ops = rpcOps(username, credential) + return CordaSSHAuthInfo(true, ops) + } catch (e: ActiveMQSecurityException) { + logger.warn(e.message) + } catch (e: Exception) { + logger.warn(e.message, e) + } + return AuthInfo.UNSUCCESSFUL + } + + override fun getCredentialType(): Class = String::class.java +} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt new file mode 100644 index 0000000000..c8202bf03d --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt @@ -0,0 +1,15 @@ +package net.corda.tools.shell + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.core.messaging.CordaRPCOps +import net.corda.tools.shell.InteractiveShell.createYamlInputMapper +import net.corda.tools.shell.utlities.ANSIProgressRenderer +import org.crsh.auth.AuthInfo + +class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null) : AuthInfo { + override fun isSuccessful(): Boolean = successful + + val yamlInputMapper: ObjectMapper by lazy { + createYamlInputMapper(rpcOps) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt similarity index 99% rename from node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt index cb48f1b829..e001425b0a 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt @@ -1,4 +1,4 @@ -package net.corda.node.shell +package net.corda.tools.shell import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.concurrent.openFuture diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt similarity index 81% rename from node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 8cb160b908..88620c32c6 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -1,4 +1,4 @@ -package net.corda.node.shell +package net.corda.tools.shell import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -9,11 +9,11 @@ import com.google.common.io.Closeables import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.StringToMethodCallParser import net.corda.client.rpc.PermissionException +import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowLogic -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture @@ -23,18 +23,10 @@ import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.StateMachineUpdate import net.corda.core.node.NodeInfo -import net.corda.core.node.services.IdentityService import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.internal.Node -import net.corda.node.internal.StartedNode -import net.corda.node.internal.security.AdminSubject -import net.corda.node.internal.security.RPCSecurityManager -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT -import net.corda.node.services.messaging.RpcAuthContext -import net.corda.node.utilities.ANSIProgressRenderer -import net.corda.node.utilities.StdoutANSIProgressRenderer -import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.tools.shell.utlities.ANSIProgressRenderer +import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import org.crsh.command.InvocationContext import org.crsh.console.jline.JLineProcessor import org.crsh.console.jline.TerminalFactory @@ -60,6 +52,7 @@ import rx.Observable import rx.Subscriber import java.io.* import java.lang.reflect.InvocationTargetException +import java.lang.reflect.UndeclaredThrowableException import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -80,61 +73,98 @@ import kotlin.concurrent.thread // TODO: Resurrect or reimplement the mail plugin. // TODO: Make it notice new shell commands added after the node started. +data class SSHDConfiguration(val port: Int) { + companion object { + internal const val INVALID_PORT_FORMAT = "Invalid port: %s" + private const val MISSING_PORT_FORMAT = "Missing port: %s" + + /** + * Parses a string of the form port into a [SSHDConfiguration]. + * @throws IllegalArgumentException if the port is missing or the string is garbage. + */ + @JvmStatic + fun parse(str: String): SSHDConfiguration { + require(!str.isNullOrBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) } + val port = try { + str.toInt() + } catch (ex: NumberFormatException) { + throw IllegalArgumentException("Port syntax is invalid, expected port") + } + return SSHDConfiguration(port) + } + } + + init { + require(port in (0..0xffff)) { INVALID_PORT_FORMAT.format(port) } + } +} + +data class ShellSslOptions(override val sslKeystore: Path, override val keyStorePassword: String, override val trustStoreFile:Path, override val trustStorePassword: String) : SSLConfiguration { + override val certificatesDirectory: Path get() = Paths.get("") +} + +data class ShellConfiguration( + val commandsDirectory: Path, + val cordappsDirectory: Path? = null, + var user: String = "", + var password: String = "", + val hostAndPort: NetworkHostAndPort, + val ssl: ShellSslOptions? = null, + val sshdPort: Int? = null, + val sshHostKeyDirectory: Path? = null, + val noLocalShell: Boolean = false) { + companion object { + const val SSH_PORT = 2222 + const val COMMANDS_DIR = "shell-commands" + const val CORDAPPS_DIR = "cordapps" + const val SSHD_HOSTKEY_DIR = "ssh" + } +} + object InteractiveShell { private val log = LoggerFactory.getLogger(javaClass) - private lateinit var node: StartedNode - @VisibleForTesting - internal lateinit var database: CordaPersistence - private lateinit var rpcOps: CordaRPCOps - private lateinit var securityManager: RPCSecurityManager - private lateinit var identityService: IdentityService + private lateinit var rpcOps: (username: String, credentials: String) -> CordaRPCOps + private lateinit var connection: CordaRPCOps private var shell: Shell? = null - private lateinit var nodeLegalName: CordaX500Name - + private var classLoader: ClassLoader? = null /** * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node * internals. */ - fun startShell(configuration: NodeConfiguration, cordaRPCOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) { - this.rpcOps = cordaRPCOps - this.securityManager = securityManager - this.identityService = identityService - this.nodeLegalName = configuration.myLegalName - this.database = database - val dir = configuration.baseDirectory - val runSshDaemon = configuration.sshd != null + fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { + rpcOps = { username: String, credentials: String -> + val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort, + sslConfiguration = configuration.ssl, classLoader = classLoader) + client.start(username, credentials).proxy + } + InteractiveShell.classLoader = classLoader + val runSshDaemon = configuration.sshdPort != null val config = Properties() if (runSshDaemon) { - val sshKeysDir = dir / "sshkey" - sshKeysDir.toFile().mkdirs() - // Enable SSH access. Note: these have to be strings, even though raw object assignments also work. - config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString() - config["crash.ssh.keygen"] = "true" - config["crash.ssh.port"] = configuration.sshd?.port.toString() + config["crash.ssh.port"] = configuration.sshdPort?.toString() config["crash.auth"] = "corda" + configuration.sshHostKeyDirectory?.apply { + val sshKeysDir = configuration.sshHostKeyDirectory + sshKeysDir.toFile().mkdirs() + config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString() + config["crash.ssh.keygen"] = "true" + } } ExternalResolver.INSTANCE.addCommand("run", "Runs a method from the CordaRPCOps interface on the node.", RunShellCommand::class.java) ExternalResolver.INSTANCE.addCommand("flow", "Commands to work with flows. Flows are how you can change the ledger.", FlowShellCommand::class.java) ExternalResolver.INSTANCE.addCommand("start", "An alias for 'flow start'", StartShellCommand::class.java) - shell = ShellLifecycle(dir).start(config) - - if (runSshDaemon) { - Node.printBasicNodeInfo("SSH server listening on port", configuration.sshd!!.port.toString()) - } + shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password) } - fun runLocalShell(node: StartedNode) { + fun runLocalShell(onExit: () -> Unit = {}) { val terminal = TerminalFactory.create() val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) InterruptHandler { jlineProcessor.interrupt() }.install() thread(name = "Command line shell processor", isDaemon = true) { - // Give whoever has local shell access administrator access to the node. - val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")) - CURRENT_RPC_CONTEXT.set(context) Emoji.renderIfSupported { jlineProcessor.run() } @@ -144,22 +174,22 @@ object InteractiveShell { jlineProcessor.closed() log.info("Command shell has exited") terminal.restore() - node.dispose() + onExit.invoke() } } - class ShellLifecycle(val dir: Path) : PluginLifeCycle() { - fun start(config: Properties): Shell { + class ShellLifecycle(private val shellCommands: Path) : PluginLifeCycle() { + fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell { val classLoader = this.javaClass.classLoader val classpathDriver = ClassPathMountFactory(classLoader) val fileDriver = FileMountFactory(Utils.getCurrentDirectory()) - val extraCommandsPath = (dir / "shell-commands").toAbsolutePath().createDirectories() + val extraCommandsPath = shellCommands.toAbsolutePath().createDirectories() val commandsFS = FS.Builder() .register("file", fileDriver) .mount("file:" + extraCommandsPath) .register("classpath", classpathDriver) - .mount("classpath:/net/corda/node/shell/") + .mount("classpath:/net/corda/tools/shell/") .mount("classpath:/crash/commands/") .build() val confFS = FS.Builder() @@ -172,25 +202,29 @@ object InteractiveShell { // Don't use the Java language plugin (we may not have tools.jar available at runtime), this // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that // is only the 'jmx' command. - return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, securityManager, nodeLegalName) + return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps) } } - val attributes = mapOf( - "ops" to rpcOps, - "mapper" to yamlInputMapper - ) + val attributes = emptyMap() val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader) context.refresh() this.config = config start(context) - return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")), StdoutANSIProgressRenderer)) + connection = makeRPCOps(rpcOps, localUserName, localUserPassword) + return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, connection, StdoutANSIProgressRenderer)) } } - private val yamlInputMapper: ObjectMapper by lazy { + fun nodeInfo() = try { + connection.nodeInfo() + } catch (e: UndeclaredThrowableException) { + throw e.cause ?: e + } + + fun createYamlInputMapper(rpcOps: CordaRPCOps): ObjectMapper { // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra // serializers. - JacksonSupport.createInMemoryMapper(identityService, YAMLFactory(), true).apply { + return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply { val rpcModule = SimpleModule() rpcModule.addDeserializer(InputStream::class.java, InputStreamDeserializer) rpcModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) @@ -244,8 +278,13 @@ object InteractiveShell { * the [runFlowFromString] method and starts the requested flow. Ctrl-C can be used to cancel. */ @JvmStatic - fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer) { - val matches = rpcOps.registeredFlows().filter { nameFragment in it } + fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer, om: ObjectMapper) { + val matches = try { + rpcOps.registeredFlows().filter { nameFragment in it } + } catch (e: PermissionException) { + output.println(e.message ?: "Access denied", Color.red) + return + } if (matches.isEmpty()) { output.println("No matching flow found, run 'flow list' to see your options.", Color.red) return @@ -255,11 +294,15 @@ object InteractiveShell { return } - val clazz: Class> = uncheckedCast(Class.forName(matches.single())) + val flowClazz: Class> = if (classLoader != null) { + uncheckedCast(Class.forName(matches.single(), true, classLoader)) + } else { + uncheckedCast(Class.forName(matches.single())) + } try { // Show the progress tracker on the console until the flow completes or is interrupted with a // Ctrl-C keypress. - val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, clazz) + val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om) val latch = CountDownLatch(1) ansiProgressRenderer.render(stateObservable, { latch.countDown() }) @@ -298,7 +341,7 @@ object InteractiveShell { fun runFlowFromString(invoke: (Class>, Array) -> FlowProgressHandle, inputData: String, clazz: Class>, - om: ObjectMapper = yamlInputMapper): FlowProgressHandle { + om: ObjectMapper): FlowProgressHandle { // For each constructor, attempt to parse the input data as a method call. Use the first that succeeds, // and keep track of the reasons we failed so we can print them out if no constructors are usable. val parser = StringToMethodCallParser(clazz, om) @@ -312,10 +355,8 @@ object InteractiveShell { try { // Attempt construction with the given arguments. - val args = database.transaction { - paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) - parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.parameterTypes), inputData) - } + paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) + val args = parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.parameterTypes), inputData) if (args.size != ctor.parameterTypes.size) { errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)") continue @@ -348,9 +389,7 @@ object InteractiveShell { val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed() val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) } val subscriber = FlowWatchPrintingSubscriber(out) - database.transaction { - stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber) - } + stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber) var result: Any? = subscriber.future if (result is Future<*>) { if (!result.isDone) { @@ -372,9 +411,7 @@ object InteractiveShell { } @JvmStatic - fun runRPCFromString(input: List, out: RenderPrintWriter, context: InvocationContext, cordaRPCOps: CordaRPCOps): Any? { - val parser = StringToMethodCallParser(CordaRPCOps::class.java, context.attributes["mapper"] as ObjectMapper) - + fun runRPCFromString(input: List, out: RenderPrintWriter, context: InvocationContext, cordaRPCOps: CordaRPCOps, om: ObjectMapper): Any? { val cmd = input.joinToString(" ").trim { it <= ' ' } if (cmd.toLowerCase().startsWith("startflow")) { // The flow command provides better support and startFlow requires special handling anyway due to @@ -387,7 +424,8 @@ object InteractiveShell { var result: Any? = null try { InputStreamSerializer.invokeContext = context - val call = database.transaction { parser.parse(cordaRPCOps, cmd) } + val parser = StringToMethodCallParser(CordaRPCOps::class.java, om) + val call = parser.parse(cordaRPCOps, cmd) result = call.call() if (result != null && result !is kotlin.Unit && result !is Void) { result = printAndFollowRPCResponse(result, out) diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShellCommand.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt similarity index 57% rename from node/src/main/kotlin/net/corda/node/shell/InteractiveShellCommand.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt index 9278fffb95..5538838c27 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShellCommand.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt @@ -1,8 +1,5 @@ -package net.corda.node.shell +package net.corda.tools.shell -import com.fasterxml.jackson.databind.ObjectMapper -import net.corda.core.messaging.CordaRPCOps -import net.corda.node.services.api.ServiceHubInternal import org.crsh.command.BaseCommand import org.crsh.shell.impl.command.CRaSHSession @@ -12,6 +9,5 @@ import org.crsh.shell.impl.command.CRaSHSession open class InteractiveShellCommand : BaseCommand() { fun ops() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).rpcOps fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer - fun services() = context.attributes["services"] as ServiceHubInternal - fun objectMapper() = context.attributes["mapper"] as ObjectMapper + fun objectMapper() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).yamlInputMapper } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt new file mode 100644 index 0000000000..bca4ad47c5 --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt @@ -0,0 +1,21 @@ +package net.corda.tools.shell + +import net.corda.core.messaging.CordaRPCOps +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Proxy + +fun makeRPCOps(getCordaRPCOps: (username: String, credential: String) -> CordaRPCOps, username: String, credential: String): CordaRPCOps { + val cordaRPCOps: CordaRPCOps by lazy { + getCordaRPCOps(username, credential) + } + + return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { _, method, args -> + try { + method.invoke(cordaRPCOps, *(args ?: arrayOf())) + } catch (e: InvocationTargetException) { + // Unpack exception. + throw e.targetException + } + } + ) as CordaRPCOps +} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt new file mode 100644 index 0000000000..16613e27a1 --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt @@ -0,0 +1,110 @@ +package net.corda.tools.shell + +import com.jcabi.manifests.Manifests +import joptsimple.OptionException +import net.corda.core.internal.* +import org.fusesource.jansi.Ansi +import org.fusesource.jansi.AnsiConsole +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Path +import java.util.concurrent.CountDownLatch +import kotlin.streams.toList +import java.io.IOException +import java.io.BufferedReader +import java.io.InputStreamReader +import kotlin.system.exitProcess + +fun main(args: Array) { + + val argsParser = CommandLineOptionParser() + val cmdlineOptions = try { + argsParser.parse(*args) + } catch (e: OptionException) { + println("Invalid command line arguments: ${e.message}") + argsParser.printHelp(System.out) + exitProcess(1) + } + + if (cmdlineOptions.help) { + argsParser.printHelp(System.out) + return + } + val config = try { + cmdlineOptions.toConfig() + } catch(e: Exception) { + println("Configuration exception: ${e.message}") + exitProcess(1) + } + StandaloneShell(config).run() +} + +class StandaloneShell(private val configuration: ShellConfiguration) { + + private fun getCordappsInDirectory(cordappsDir: Path?): List = + if (cordappsDir == null || !cordappsDir.exists()) { + emptyList() + } else { + cordappsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() + } + } + + //Workaround in case console is not available + @Throws(IOException::class) + private fun readLine(format: String, vararg args: Any): String { + if (System.console() != null) { + return System.console().readLine(format, *args) + } + print(String.format(format, *args)) + val reader = BufferedReader(InputStreamReader(System.`in`)) + return reader.readLine() + } + + @Throws(IOException::class) + private fun readPassword(format: String, vararg args: Any) = + if (System.console() != null) System.console().readPassword(format, *args) else this.readLine(format, *args).toCharArray() + + private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown" + + fun run() { + val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory) + val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) + with(configuration) { + if (user.isNullOrEmpty()) { + user = readLine("User:") + } + if (password.isNullOrEmpty()) { + password = String(readPassword("Password:")) + } + } + InteractiveShell.startShell(configuration, classLoader) + try { + //connecting to node by requesting node info to fail fast + InteractiveShell.nodeInfo() + } catch (e: Exception) { + println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"") + exitProcess(1) + } + + val exit = CountDownLatch(1) + AnsiConsole.systemInstall() + println(Ansi.ansi().fgBrightRed().a( + """ ______ __""").newline().a( + """ / ____/ _________/ /___ _""").newline().a( + """ / / __ / ___/ __ / __ `/ """).newline().fgBrightRed().a( + """/ /___ /_/ / / / /_/ / /_/ /""").newline().fgBrightRed().a( + """\____/ /_/ \__,_/\__,_/""").reset().fgBrightDefault().bold() + .newline().a("--- ${getManifestEntry("Corda-Vendor")} ${getManifestEntry("Corda-Release-Version")} (${getManifestEntry("Corda-Revision").take(7)}) ---") + .newline() + .newline().a("Standalone Shell connected to ${configuration.hostAndPort}") + .reset()) + InteractiveShell.runLocalShell { + exit.countDown() + } + configuration.sshdPort?.apply{ println("SSH server listening on port $this.") } + + exit.await() + exitProcess(0) + } +} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt new file mode 100644 index 0000000000..736b922afc --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt @@ -0,0 +1,226 @@ +package net.corda.tools.shell + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import joptsimple.OptionParser +import joptsimple.util.EnumConverter +import net.corda.core.internal.div +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.parseAs +import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR +import org.slf4j.event.Level +import java.io.PrintStream +import java.nio.file.Path +import java.nio.file.Paths + +// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup. +class CommandLineOptionParser { + private val optionParser = OptionParser() + + private val configFileArg = optionParser + .accepts("config-file", "The path to the shell configuration file, used instead of providing the rest of command line options.") + .withOptionalArg() + private val cordappsDirectoryArg = optionParser + .accepts("cordpass-directory", "The path to directory containing Cordapps jars, Cordapps are require when starting flows.") + .withOptionalArg() + private val commandsDirectoryArg = optionParser + .accepts("commands-directory", "The directory with additional CrAsH shell commands.") + .withOptionalArg() + private val hostArg = optionParser + .acceptsAll(listOf("h","host"), "The host of the Corda node.") + .withRequiredArg() + private val portArg = optionParser + .acceptsAll(listOf("p","port"), "The port of the Corda node.") + .withRequiredArg() + private val userArg = optionParser + .accepts("user", "The RPC user name.") + .withOptionalArg() + private val passwordArg = optionParser + .accepts("password", "The RPC user password.") + .withOptionalArg() + private val loggerLevel = optionParser + .accepts("logging-level", "Enable logging at this level and higher.") + .withRequiredArg() + .withValuesConvertedBy(object : EnumConverter(Level::class.java) {}) + .defaultsTo(Level.INFO) + private val sshdPortArg = optionParser + .accepts("sshd-port", "Enables SSH server for shell.") + .withOptionalArg() + private val sshdHostKeyDirectoryArg = optionParser + .accepts("sshd-hostkey-directory", "The directory with hostkey.pem file for SSH server.") + .withOptionalArg() + private val helpArg = optionParser + .accepts("help") + .forHelp() + private val keyStorePasswordArg = optionParser + .accepts("keystore-password", "The password to unlock the KeyStore file.") + .withOptionalArg() + private val keyStoreDirArg = optionParser + .accepts("keystore-file", "The path to the KeyStore file.") + .withOptionalArg() + private val keyStoreTypeArg = optionParser + .accepts("keystore-type", "The type of the KeyStore (e.g. JKS).") + .withOptionalArg() + private val trustStorePasswordArg = optionParser + .accepts("truststore-password", "The password to unlock the TrustStore file.") + .withOptionalArg() + private val trustStoreDirArg = optionParser + .accepts("truststore-file", "The path to the TrustStore file.") + .withOptionalArg() + private val trustStoreTypeArg = optionParser + .accepts("truststore-type", "The type of the TrustStore (e.g. JKS).") + .withOptionalArg() + + fun parse(vararg args: String): CommandLineOptions { + val optionSet = optionParser.parse(*args) + return CommandLineOptions( + configFile = optionSet.valueOf(configFileArg), + host = optionSet.valueOf(hostArg), + port = optionSet.valueOf(portArg), + user = optionSet.valueOf(userArg), + password = optionSet.valueOf(passwordArg), + commandsDirectory = (optionSet.valueOf(commandsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, + cordappsDirectory = (optionSet.valueOf(cordappsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, + help = optionSet.has(helpArg), + loggingLevel = optionSet.valueOf(loggerLevel), + sshdPort = optionSet.valueOf(sshdPortArg), + sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, + keyStorePassword = optionSet.valueOf(keyStorePasswordArg), + trustStorePassword = optionSet.valueOf(trustStorePasswordArg), + keyStoreFile = (optionSet.valueOf(keyStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, + trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, + keyStoreType = optionSet.valueOf(keyStoreTypeArg), + trustStoreType = optionSet.valueOf(trustStoreTypeArg)) + } + + fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) +} + +data class CommandLineOptions(val configFile: String?, + val commandsDirectory: Path?, + val cordappsDirectory: Path?, + val host: String?, + val port: String?, + val user: String?, + val password: String?, + val help: Boolean, + val loggingLevel: Level, + val sshdPort: String?, + val sshdHostKeyDirectory: Path?, + val keyStorePassword: String?, + val trustStorePassword: String?, + val keyStoreFile: Path?, + val trustStoreFile: Path?, + val keyStoreType: String?, + val trustStoreType: String?) { + + private fun toConfigFile(): Config { + val cmdOpts = mutableMapOf() + + commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } + cordappsDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } + user?.apply { cmdOpts["node.user"] = this } + password?.apply { cmdOpts["node.password"] = this } + host?.apply { cmdOpts["node.addresses.rpc.host"] = this } + port?.apply { cmdOpts["node.addresses.rpc.port"] = this } + keyStoreFile?.apply { cmdOpts["ssl.keystore.path"] = this.toString() } + keyStorePassword?.apply { cmdOpts["ssl.keystore.password"] = this } + keyStoreType?.apply { cmdOpts["ssl.keystore.type"] = this } + trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } + trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } + trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } + sshdPort?.apply { + cmdOpts["extensions.sshd.port"] = this + cmdOpts["extensions.sshd.enabled"] = true + } + sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() } + + return ConfigFactory.parseMap(cmdOpts) + } + + /** Return configuration parsed from an optional config file (provided by the command line option) + * and then overridden by the command line options */ + fun toConfig(): ShellConfiguration { + val fileConfig = configFile?.let { ConfigFactory.parseFile(Paths.get(configFile).toFile()) } + ?: ConfigFactory.empty() + val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() + val shellConfigFile = typeSafeConfig.parseAs() + return shellConfigFile.toShellConfiguration() + } +} + +/** Object representation of Shell configuration file */ +private class ShellConfigurationFile { + data class Rpc( + val host: String, + val port: Int) + + data class Addresses( + val rpc: Rpc + ) + + data class Node( + val addresses: Addresses, + val user: String?, + val password: String? + ) + + data class Cordapps( + val path: String + ) + + data class Sshd( + val enabled: Boolean, + val port: Int, + val hostkeypath: String? + ) + + data class Commands( + val path: String + ) + + data class Extensions( + val cordapps: Cordapps, + val sshd: Sshd, + val commands: Commands? + ) + + data class KeyStore( + val path: String, + val type: String, + val password: String + ) + + data class Ssl( + val keystore: KeyStore, + val truststore: KeyStore + ) + + data class ShellConfigFile( + val node: Node, + val extensions: Extensions?, + val ssl: Ssl? + ) { + fun toShellConfiguration(): ShellConfiguration { + + val sslOptions = + ssl?.let { + ShellSslOptions( + sslKeystore = Paths.get(it.keystore.path), + keyStorePassword = it.keystore.password, + trustStoreFile = Paths.get(it.truststore.path), + trustStorePassword = it.truststore.password) + } + + return ShellConfiguration( + commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") / COMMANDS_DIR, + cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, + user = node.user ?: "", + password = node.password ?: "", + hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), + ssl = sslOptions, + sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null }, + sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null }) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt similarity index 99% rename from node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt index 74c8e077c4..4848ff5d47 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt @@ -1,4 +1,4 @@ -package net.corda.node.utilities +package net.corda.tools.shell.utlities import net.corda.core.internal.Emoji import net.corda.core.messaging.FlowProgressHandle diff --git a/node/src/main/resources/net/corda/node/shell/base/login.groovy b/tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy similarity index 90% rename from node/src/main/resources/net/corda/node/shell/base/login.groovy rename to tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy index f6df32b386..4aa1823a00 100644 --- a/node/src/main/resources/net/corda/node/shell/base/login.groovy +++ b/tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy @@ -1,4 +1,4 @@ -package net.corda.node.shell.base +package net.corda.tools.shell.base // Note that this file MUST be in a sub-directory called "base" relative to the path // given in the configuration code in InteractiveShell. diff --git a/node/src/test/kotlin/net/corda/node/shell/CustomTypeJsonParsingTests.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt similarity index 98% rename from node/src/test/kotlin/net/corda/node/shell/CustomTypeJsonParsingTests.kt rename to tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt index bd9eee844d..7ab1718558 100644 --- a/node/src/test/kotlin/net/corda/node/shell/CustomTypeJsonParsingTests.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt @@ -1,4 +1,4 @@ -package net.corda.node.shell +package net.corda.tools.shell import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.ObjectMapper diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt similarity index 81% rename from node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt rename to tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index 79a6769d40..033bd3ba8f 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -1,4 +1,4 @@ -package net.corda.node +package net.corda.tools.shell import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import net.corda.client.jackson.JacksonSupport @@ -11,15 +11,9 @@ import net.corda.core.identity.Party import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.utilities.ProgressTracker -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.node.shell.InteractiveShell -import net.corda.node.internal.configureDatabase +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.core.TestIdentity -import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestIdentityService -import net.corda.testing.internal.rigorousMock -import org.junit.After -import org.junit.Before import org.junit.Test import rx.Observable import java.util.* @@ -30,16 +24,6 @@ class InteractiveShellTest { private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) } - @Before - fun setup() { - InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock()) - } - - @After - fun shutdown() { - InteractiveShell.database.close() - } - @Suppress("UNUSED") class FlowA(val a: String) : FlowLogic() { constructor(b: Int?) : this(b.toString()) @@ -52,7 +36,7 @@ class InteractiveShellTest { override fun call() = a } - private val ids = makeTestIdentityService(megaCorp.identity) + private val ids = InMemoryIdentityService(arrayOf(megaCorp.identity), DEV_ROOT_CA.certificate) private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory()) private fun check(input: String, expected: String) { diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt new file mode 100644 index 0000000000..4ac40cf64c --- /dev/null +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -0,0 +1,204 @@ +package net.corda.tools.shell + +import net.corda.core.utilities.NetworkHostAndPort +import org.junit.Test +import org.slf4j.event.Level +import java.nio.file.Paths +import kotlin.test.assertEquals +import java.io.File + +class StandaloneShellArgsParserTest { + + private val CONFIG_FILE = File(javaClass.classLoader.getResource("config.conf")!!.file) + + @Test + fun args_to_cmd_options() { + + val args = arrayOf("--config-file", "/x/y/z/config.conf", + "--commands-directory", "/x/y/commands", + "--cordpass-directory", "/x/y/cordapps", + "--host", "alocalhost", + "--port", "1234", + "--user", "demo", + "--password", "abcd1234", + "--logging-level", "DEBUG", + "--sshd-port", "2223", + "--sshd-hostkey-directory", "/x/y/ssh", + "--help", + "--keystore-password", "pass1", + "--truststore-password", "pass2", + "--keystore-file", "/x/y/keystore.jks", + "--truststore-file", "/x/y/truststore.jks", + "--truststore-type", "dummy", + "--keystore-type", "JKS") + + val expectedOptions = CommandLineOptions(configFile = "/x/y/z/config.conf", + commandsDirectory = Paths.get("/x/y/commands"), + cordappsDirectory = Paths.get("/x/y/cordapps"), + host = "alocalhost", + port = "1234", + user = "demo", + password = "abcd1234", + help = true, + loggingLevel = Level.DEBUG, + sshdPort = "2223", + sshdHostKeyDirectory = Paths.get("/x/y/ssh"), + keyStorePassword = "pass1", + trustStorePassword = "pass2", + keyStoreFile = Paths.get("/x/y/keystore.jks"), + trustStoreFile = Paths.get("/x/y/truststore.jks"), + trustStoreType = "dummy", + keyStoreType = "JKS") + + val options = CommandLineOptionParser().parse(*args) + + assertEquals(expectedOptions, options) + } + + @Test + fun empty_args_to_cmd_options() { + val args = emptyArray() + + val expectedOptions = CommandLineOptions(configFile = null, + commandsDirectory = null, + cordappsDirectory = null, + host = null, + port = null, + user = null, + password = null, + help = false, + loggingLevel = Level.INFO, + sshdPort = null, + sshdHostKeyDirectory = null, + keyStorePassword = null, + trustStorePassword = null, + keyStoreFile = null, + trustStoreFile = null, + trustStoreType = null, + keyStoreType = null) + + val options = CommandLineOptionParser().parse(*args) + + assertEquals(expectedOptions, options) + } + + @Test + fun args_to_config() { + + val options = CommandLineOptions(configFile = null, + commandsDirectory = Paths.get("/x/y/commands"), + cordappsDirectory = Paths.get("/x/y/cordapps"), + host = "alocalhost", + port = "1234", + user = "demo", + password = "abcd1234", + help = true, + loggingLevel = Level.DEBUG, + sshdPort = "2223", + sshdHostKeyDirectory = Paths.get("/x/y/ssh"), + keyStorePassword = "pass1", + trustStorePassword = "pass2", + keyStoreFile = Paths.get("/x/y/keystore.jks"), + trustStoreFile = Paths.get("/x/y/truststore.jks"), + keyStoreType = "dummy", + trustStoreType = "dummy" + ) + + val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"), + keyStorePassword = "pass1", + trustStoreFile = Paths.get("/x/y/truststore.jks"), + trustStorePassword = "pass2") + val expectedConfig = ShellConfiguration( + commandsDirectory = Paths.get("/x/y/commands"), + cordappsDirectory = Paths.get("/x/y/cordapps"), + user = "demo", + password = "abcd1234", + hostAndPort = NetworkHostAndPort("alocalhost", 1234), + ssl = expectedSsl, + sshdPort = 2223, + sshHostKeyDirectory = Paths.get("/x/y/ssh"), + noLocalShell = false) + + val config = options.toConfig() + + assertEquals(expectedConfig, config) + } + + @Test + fun acmd_options_to_config_from_file() { + + val options = CommandLineOptions(configFile = CONFIG_FILE.absolutePath, + commandsDirectory = null, + cordappsDirectory = null, + host = null, + port = null, + user = null, + password = null, + help = false, + loggingLevel = Level.DEBUG, + sshdPort = null, + sshdHostKeyDirectory = null, + keyStorePassword = null, + trustStorePassword = null, + keyStoreFile = null, + trustStoreFile = null, + keyStoreType = null, + trustStoreType = null) + + val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"), + keyStorePassword = "pass1", + trustStoreFile = Paths.get("/x/y/truststore.jks"), + trustStorePassword = "pass2") + val expectedConfig = ShellConfiguration( + commandsDirectory = Paths.get("/x/y/commands"), + cordappsDirectory = Paths.get("/x/y/cordapps"), + user = "demo", + password = "abcd1234", + hostAndPort = NetworkHostAndPort("alocalhost", 1234), + ssl = expectedSsl, + sshdPort = 2223) + + val config = options.toConfig() + + assertEquals(expectedConfig, config) + } + + @Test + fun cmd_options_override_config_from_file() { + + val options = CommandLineOptions(configFile = CONFIG_FILE.absolutePath, + commandsDirectory = null, + cordappsDirectory = null, + host = null, + port = null, + user = null, + password = "blabla", + help = false, + loggingLevel = Level.DEBUG, + sshdPort = null, + sshdHostKeyDirectory = null, + keyStorePassword = null, + trustStorePassword = null, + keyStoreFile = Paths.get("/x/y/cmd.jks"), + trustStoreFile = null, + keyStoreType = null, + trustStoreType = null) + + val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/cmd.jks"), + keyStorePassword = "pass1", + trustStoreFile = Paths.get("/x/y/truststore.jks"), + trustStorePassword = "pass2") + val expectedConfig = ShellConfiguration( + commandsDirectory = Paths.get("/x/y/commands"), + cordappsDirectory = Paths.get("/x/y/cordapps"), + user = "demo", + password = "blabla", + hostAndPort = NetworkHostAndPort("alocalhost", 1234), + ssl = expectedSsl, + sshdPort = 2223) + + val config = options.toConfig() + + assertEquals(expectedConfig, config) + } +} \ No newline at end of file diff --git a/tools/shell/src/test/resources/config.conf b/tools/shell/src/test/resources/config.conf new file mode 100644 index 0000000000..9a964cec18 --- /dev/null +++ b/tools/shell/src/test/resources/config.conf @@ -0,0 +1,34 @@ +node { + addresses { + rpc { + host : "alocalhost" + port : 1234 + } + } + user : demo + password : abcd1234 +} +extensions { + cordapps { + path : "/x/y/cordapps" + } + sshd { + enabled : "true" + port : 2223 + } + commands { + path : /x/y/commands + } +} +ssl { + keystore { + path : "/x/y/keystore.jks" + type : "JKS" + password : "pass1" + } + truststore { + path : "/x/y/truststore.jks" + type : "JKS" + password : "pass2" + } + } \ No newline at end of file From 4e5df2f4fbeb78702c27d12113387f649b306ce9 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 7 Mar 2018 10:39:03 +0000 Subject: [PATCH 3/7] Fix permissioning doc (#2755) --- docs/source/permissioning.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index 67c9b74c5d..81186269a3 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -78,16 +78,16 @@ public/private keypairs and certificates. The keypairs and certificates should o Certificate role extension -------------------------- Corda certificates have a custom X.509 v3 extension that specifies the role the certificate relates to. This extension -has the OID 1.3.6.1.4.1.50530.1.1 and is non-critical, so implementations outside of Corda nodes can safely ignore it. +has the OID ``1.3.6.1.4.1.50530.1.1`` and is non-critical, so implementations outside of Corda nodes can safely ignore it. The extension contains a single ASN.1 integer identifying the identity type the certificate is for: 1. Doorman 2. Network map -3. Service identity (such as a notary or oracle) -3. Node certificate authority (from which the TLS and well-known identity certificates are issued) -4. Transport layer security -5. Well-known legal identity -6. Confidential legal identity +3. Service identity (currently only used as the shared identity in distributed notaries) +4. Node certificate authority (from which the TLS and well-known identity certificates are issued) +5. Transport layer security +6. Well-known legal identity +7. Confidential legal identity In a typical installation, node administrators needn't be aware of these. However, when node certificates are managed by external tools (such as an existing PKI solution deployed within an organisation), it is important to understand From 0d5f033aaec8549fa9a9d1964dcc5cfede3abe72 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 7 Mar 2018 13:28:49 +0000 Subject: [PATCH 4/7] [CORDA-1085] Alter column names to be consistent with R3.Corda. (#2606) Alter column names to be consistent with R3.Corda - related to CORDA-990. --- .../services/persistence/NodePropertiesPersistentStore.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt index deaff88abb..afa84c9f2a 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt @@ -29,10 +29,10 @@ class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, persistenc @Table(name = "${NODE_DATABASE_PREFIX}properties") class DBNodeProperty( @Id - @Column(name = "key") + @Column(name = "property_key") val key: String = "", - @Column(name = "value") + @Column(name = "property_value") var value: String? = "" ) } From e6ed91df8b2ec9f853edd8b521a4d9809fc17b5b Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 7 Mar 2018 16:02:01 +0000 Subject: [PATCH 5/7] CORDA-1186 - Move ConstructorForDeserialization out of internal (#2759) Since it's a user facing object it shouldn't exist in an internal package. Move to core to exist with the other serialization annotations --- .../core/serialization/ConstructorForDeserialization.kt | 8 ++++++++ .../internal/serialization/amqp/SerializationHelper.kt | 8 +------- .../serialization/amqp/JavaSerializationOutputTests.java | 1 + .../internal/serialization/amqp/EvolvabilityTests.kt | 1 + .../internal/serialization/amqp/PrivatePropertyTests.kt | 3 +-- .../serialization/amqp/SerializationPropertyOrdering.kt | 1 + 6 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/core/serialization/ConstructorForDeserialization.kt diff --git a/node-api/src/main/kotlin/net/corda/core/serialization/ConstructorForDeserialization.kt b/node-api/src/main/kotlin/net/corda/core/serialization/ConstructorForDeserialization.kt new file mode 100644 index 0000000000..d3127c973f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/core/serialization/ConstructorForDeserialization.kt @@ -0,0 +1,8 @@ +package net.corda.core.serialization + +/** + * Annotation indicating a constructor to be used to reconstruct instances of a class during deserialization. + */ +@Target(AnnotationTarget.CONSTRUCTOR) +@Retention(AnnotationRetention.RUNTIME) +annotation class ConstructorForDeserialization \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index cc8591c612..cda4b76ac2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -3,6 +3,7 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.codec.Data @@ -18,13 +19,6 @@ import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaType -/** - * Annotation indicating a constructor to be used to reconstruct instances of a class during deserialization. - */ -@Target(AnnotationTarget.CONSTRUCTOR) -@Retention(AnnotationRetention.RUNTIME) -annotation class ConstructorForDeserialization - /** * Code for finding the constructor we will use for deserialization. * diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index c51ae5aff9..b6f55d3e73 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -3,6 +3,7 @@ package net.corda.nodeapi.internal.serialization.amqp; import com.google.common.collect.ImmutableList; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; +import net.corda.core.serialization.ConstructorForDeserialization; import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.core.serialization.SerializedBytes; import org.apache.qpid.proton.codec.DecoderImpl; diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index 74b175ae48..c7d9795cbb 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.SignedData import net.corda.core.crypto.sign import net.corda.core.node.NetworkParameters import net.corda.core.node.NotaryInfo +import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes import net.corda.testing.common.internal.ProjectStructure.projectRootDir diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/PrivatePropertyTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/PrivatePropertyTests.kt index 3f8efa4407..795ac1a12e 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/PrivatePropertyTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/PrivatePropertyTests.kt @@ -2,8 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertEquals -import org.slf4j.Logger -import org.slf4j.LoggerFactory +import net.corda.core.serialization.ConstructorForDeserialization import org.junit.Test import org.apache.qpid.proton.amqp.Symbol import org.assertj.core.api.Assertions diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationPropertyOrdering.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationPropertyOrdering.kt index c23572db41..2fd435aa24 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationPropertyOrdering.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationPropertyOrdering.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.ConstructorForDeserialization import org.junit.Test import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertEquals From 84e930d52a93331010535bd3f537fdbff9a150c1 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Wed, 7 Mar 2018 16:13:16 +0000 Subject: [PATCH 6/7] [CORDA-991] - Update committed api from v3 release branch (#2734) * Update from v3 * Remove CordaInternal attribute as this is removed by v4 api-scanner plugin * Make SerializationContext @DoNotImplement * Addition of CordappConfig to contstructor of CordappContext * Alteration of getOffset/getSize properties in ByteSequence, OpaqueBytes & OpaqueBytesSubSequence * Make network parameters on MockServices final --- .ci/api-current.txt | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 973310cf94..20fb45af09 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -658,25 +658,10 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex @org.jetbrains.annotations.NotNull public abstract List getServiceFlows() @org.jetbrains.annotations.NotNull public abstract List getServices() ## -@net.corda.core.DoNotImplement public interface net.corda.core.cordapp.CordappConfig - public abstract boolean exists(String) - @org.jetbrains.annotations.NotNull public abstract Object get(String) - public abstract boolean getBoolean(String) - public abstract double getDouble(String) - public abstract float getFloat(String) - public abstract int getInt(String) - public abstract long getLong(String) - @org.jetbrains.annotations.NotNull public abstract Number getNumber(String) - @org.jetbrains.annotations.NotNull public abstract String getString(String) -## -public final class net.corda.core.cordapp.CordappConfigException extends java.lang.Exception - public (String, Throwable) -## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig) @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() - @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.CordappConfig getConfig() @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() ## @net.corda.core.DoNotImplement public interface net.corda.core.cordapp.CordappProvider @@ -1897,7 +1882,6 @@ public @interface net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) - @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public abstract java.time.Clock getClock() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.KeyManagementService getKeyManagementService() @@ -2893,9 +2877,6 @@ public @interface net.corda.core.serialization.CordaSerializationTransformRename public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization public abstract int version() ## -@net.corda.core.DoNotImplement public interface net.corda.core.serialization.EncodingWhitelist - public abstract boolean acceptEncoding(net.corda.core.serialization.SerializationEncoding) -## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.serialization.MissingAttachmentsException extends net.corda.core.CordaException public (List) @org.jetbrains.annotations.NotNull public final List getIds() @@ -2916,8 +2897,6 @@ public final class net.corda.core.serialization.SerializationAPIKt extends java. ## @net.corda.core.DoNotImplement public interface net.corda.core.serialization.SerializationContext @org.jetbrains.annotations.NotNull public abstract ClassLoader getDeserializationClassLoader() - @org.jetbrains.annotations.Nullable public abstract net.corda.core.serialization.SerializationEncoding getEncoding() - @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.EncodingWhitelist getEncodingWhitelist() public abstract boolean getObjectReferencesEnabled() @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion() @org.jetbrains.annotations.NotNull public abstract Map getProperties() @@ -2925,7 +2904,6 @@ public final class net.corda.core.serialization.SerializationAPIKt extends java. @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.ClassWhitelist getWhitelist() @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withAttachmentsClassLoader(List) @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withClassLoader(ClassLoader) - @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withEncoding(net.corda.core.serialization.SerializationEncoding) @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withPreferredSerializationVersion(net.corda.core.utilities.ByteSequence) @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withProperty(Object, Object) @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withWhitelisted(Class) @@ -2949,8 +2927,6 @@ public final class net.corda.core.serialization.SerializationDefaults extends ja @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getSTORAGE_CONTEXT() public static final net.corda.core.serialization.SerializationDefaults INSTANCE ## -@net.corda.core.DoNotImplement public interface net.corda.core.serialization.SerializationEncoding -## public abstract class net.corda.core.serialization.SerializationFactory extends java.lang.Object public () public final Object asCurrent(kotlin.jvm.functions.Function1) @@ -3382,7 +3358,6 @@ public final class net.corda.core.utilities.ByteArrays extends java.lang.Object @net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.utilities.ByteSequence extends java.lang.Object implements java.lang.Comparable public int compareTo(net.corda.core.utilities.ByteSequence) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence copy() - @org.jetbrains.annotations.NotNull public final byte[] copyBytes() public boolean equals(Object) @org.jetbrains.annotations.NotNull public abstract byte[] getBytes() public final int getOffset() @@ -3392,12 +3367,9 @@ public final class net.corda.core.utilities.ByteArrays extends java.lang.Object @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, int) @org.jetbrains.annotations.NotNull public final java.io.ByteArrayInputStream open() - @org.jetbrains.annotations.NotNull public final java.nio.ByteBuffer putTo(java.nio.ByteBuffer) - @org.jetbrains.annotations.NotNull public final java.nio.ByteBuffer slice(int, int) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence subSequence(int, int) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int) @org.jetbrains.annotations.NotNull public String toString() - public final void writeTo(java.io.OutputStream) public static final net.corda.core.utilities.ByteSequence$Companion Companion ## public static final class net.corda.core.utilities.ByteSequence$Companion extends java.lang.Object @@ -4164,7 +4136,6 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) - @org.jetbrains.annotations.NotNull public net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public final net.corda.testing.services.MockAttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @@ -4219,7 +4190,6 @@ public static final class net.corda.testing.node.MockServicesKt$createMockCordaS @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) - @org.jetbrains.annotations.NotNull public net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() From d6efff7ee8978bd3055fc2e8d04e9a1ebd6098cd Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 7 Mar 2018 16:52:06 +0000 Subject: [PATCH 7/7] Post merge - added copyrights to new files. --- .../tools/shell/InteractiveShellIntegrationTest.kt | 10 ++++++++++ .../net/corda/tools/shell/CordaAuthenticationPlugin.kt | 10 ++++++++++ .../kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt | 10 ++++++++++ .../kotlin/net/corda/tools/shell/RPCOpsWithContext.kt | 10 ++++++++++ .../kotlin/net/corda/tools/shell/StandaloneShell.kt | 10 ++++++++++ .../net/corda/tools/shell/StandaloneShellArgsParser.kt | 10 ++++++++++ .../corda/tools/shell/StandaloneShellArgsParserTest.kt | 10 ++++++++++ 7 files changed, 70 insertions(+) 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 index 37b9be0e04..436d229d48 100644 --- 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 @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import com.google.common.io.Files diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt index c495292d0f..77a282ff1c 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import net.corda.core.messaging.CordaRPCOps diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt index c8202bf03d..5de255c96a 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import com.fasterxml.jackson.databind.ObjectMapper diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt index bca4ad47c5..5f847b37d9 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import net.corda.core.messaging.CordaRPCOps diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt index 16613e27a1..a6c51c8047 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import com.jcabi.manifests.Manifests diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt index 736b922afc..b1eb0fe450 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import com.typesafe.config.Config diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt index 4ac40cf64c..39f18b02b4 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -1,3 +1,13 @@ +/* + * R3 Proprietary and Confidential + * + * Copyright (c) 2018 R3 Limited. All rights reserved. + * + * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. + * + * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + */ + package net.corda.tools.shell import net.corda.core.utilities.NetworkHostAndPort