diff --git a/.ci/api-current.txt b/.ci/api-current.txt index d44e533375..8d1fc55cd4 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 @@ -1908,7 +1893,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() @@ -2907,9 +2891,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() @@ -2930,8 +2911,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() @@ -2939,7 +2918,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) @@ -2963,8 +2941,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) @@ -3396,7 +3372,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() @@ -3406,12 +3381,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 @@ -4193,7 +4165,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() @@ -4252,7 +4223,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() diff --git a/.idea/compiler.xml b/.idea/compiler.xml index cd0844b545..5d2effcb9a 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -135,6 +135,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 ab2a1936b9..3ee87d9696 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,7 @@ buildscript { ext.ghostdriver_version = '2.1.0' ext.eaagentloader_version = '1.0.3' ext.curator_version = '4.0.0' + 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 3c99e2da0b..0d0dcbe427 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 @@ -83,7 +83,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) @@ -96,6 +97,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 { @@ -103,7 +113,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. } @@ -113,7 +123,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 ccc7539011..45a8f31463 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 @@ -20,4 +20,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 c58d402228..6558f3429f 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 @@ -43,18 +43,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/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 diff --git a/docs/source/shell.rst b/docs/source/shell.rst index c090272c85..b89fb28bf9 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. .. _ssh_server: @@ -44,8 +55,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 @@ -71,7 +82,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! @@ -80,14 +91,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-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 177e41147a..b0726793ac 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 @@ -13,6 +13,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 @@ -28,13 +29,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 b5b8392724..69e2931446 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 @@ -13,6 +13,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 e01ae8feee..7af638b59f 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 @@ -15,6 +15,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 6fd6508725..dad1a4696d 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 @@ -12,8 +12,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 a29b5be076..ccc4dde1a9 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 @@ -10,6 +10,7 @@ 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 diff --git a/node/build.gradle b/node/build.gradle index eb77825cef..aa583b1529 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -76,6 +76,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) @@ -113,10 +114,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" compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.1.2' @@ -163,17 +160,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" @@ -191,9 +177,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 36703d8e1e..71f1ffab1d 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 @@ -15,12 +15,12 @@ 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.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME +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.driver import net.corda.testing.driver.internal.RandomFree 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 22f4e7e1e5..dfecf078e6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -52,6 +52,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 @@ -66,7 +67,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 @@ -79,6 +79,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 @@ -269,7 +270,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, @@ -307,9 +308,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 4a5889ef66..5680707bfa 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -11,6 +11,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 @@ -36,9 +37,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 @@ -170,7 +170,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) { @@ -414,11 +414,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 6a8b972ec7..524675b42f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -22,12 +22,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 net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole @@ -153,7 +154,6 @@ open class NodeStartup(val args: Array) { node.generateAndSaveNodeInfo() return } - val startedNode = node.start() Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name }) startedNode.internals.nodeReadyFuture.thenMatch({ @@ -165,12 +165,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 cc9b25b378..a0b023b35a 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 @@ -25,6 +25,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag 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 @@ -311,8 +312,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 @@ -348,6 +347,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?) { @@ -375,6 +376,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 a724786737..be5776387c 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 @@ -15,7 +15,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/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index f17e8c5bf7..0fa9d30bfa 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 @@ -103,7 +103,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 diff --git a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt b/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt index 8df65f6ff9..e69de29bb2 100644 --- a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt +++ b/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt @@ -1,58 +0,0 @@ -/* - * 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.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 d3b357daf0..fd24f48ceb 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 @@ -13,6 +13,7 @@ package net.corda.node.services.config import com.zaxxer.hikari.HikariConfig import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort +import net.corda.tools.shell.SSHDConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.testing.core.ALICE_NAME import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -86,7 +87,7 @@ class NodeConfigurationImplTest { return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions) } - private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl { + private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl { return testConfiguration.copy(dataSourceProperties = dataSourceProperties) } 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 1179198976..8d6ae40791 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 @@ -21,12 +21,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 a6232772cc..d579647f04 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,6 +56,7 @@ include 'tools:graphs' include 'tools:bootstrapper' include 'tools:dbmigration' include 'tools:notaryhealthcheck' +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 aa0ca652cb..c44d4d30ff 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 @@ -288,7 +288,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 e185a82ea3..5d9dd83d30 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 @@ -8,11 +8,11 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 @@ -84,12 +84,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 @@ -192,7 +193,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 1cc804f96c..b90277839a 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 @@ -19,7 +19,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 @@ -131,7 +130,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(), @@ -140,7 +139,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..436d229d48 --- /dev/null +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -0,0 +1,249 @@ +/* + * 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 +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 17f8e9188d..c09687b93f 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 @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -package net.corda.node +package net.corda.tools.shell import co.paralleluniverse.fibers.Suspendable import com.jcraft.jsch.ChannelExec @@ -18,9 +18,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.core.DUMMY_NOTARY_NAME @@ -35,7 +37,6 @@ import org.bouncycastle.util.io.Streams import org.junit.ClassRule import org.junit.Test import java.net.ConnectException -import java.util.regex.Pattern import kotlin.test.assertTrue import kotlin.test.fail @@ -110,7 +111,8 @@ class SSHServerTest : IntegrationTest() { @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), @@ -125,12 +127,10 @@ class SSHServerTest : IntegrationTest() { 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() @@ -156,11 +156,17 @@ class SSHServerTest : IntegrationTest() { 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).size().isGreaterThanOrEqualTo(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 86% 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 976fd76698..7473c6ee22 100644 --- a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java @@ -8,13 +8,14 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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.*; @@ -22,7 +23,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" + @@ -38,7 +40,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? @@ -52,13 +54,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 a60a6406e1..934f35219d 100644 --- a/node/src/main/java/net/corda/node/shell/RunShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -package net.corda.node.shell; +package net.corda.tools.shell; import net.corda.core.messaging.*; import net.corda.client.jackson.*; @@ -40,7 +40,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 84% 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 973027771a..57692b5e3c 100644 --- a/node/src/main/java/net/corda/node/shell/StartShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java @@ -8,12 +8,12 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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.*; @@ -24,6 +24,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/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt similarity index 51% rename from node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt index c822d201ab..77a282ff1c 100644 --- a/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt @@ -8,20 +8,20 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -package net.corda.node.shell +package net.corda.tools.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 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: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin>(), AuthenticationPlugin { +class CordaAuthenticationPlugin(private val rpcOps: (username: String, credential: String) -> CordaRPCOps): CRaSHPlugin>(), AuthenticationPlugin { + + companion object { + private val logger = loggerFor() + } override fun getImplementation(): AuthenticationPlugin = this @@ -32,10 +32,13 @@ class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val sec 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)) + 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 } diff --git a/node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt similarity index 68% rename from node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt rename to tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt index 283ce79e57..5de255c96a 100644 --- a/node/src/main/kotlin/net/corda/node/shell/CordaSSHAuthInfo.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt @@ -8,12 +8,18 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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.utilities.ANSIProgressRenderer +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 186c24c8e4..ccc6e0cc6f 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 bc385c79d4..fd1ad5e47d 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -package net.corda.node.shell +package net.corda.tools.shell import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -19,11 +19,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 @@ -33,18 +33,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 @@ -70,6 +62,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 @@ -90,61 +83,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() } @@ -154,22 +184,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() @@ -182,25 +212,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) @@ -254,8 +288,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 @@ -265,11 +304,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() }) @@ -308,7 +351,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) @@ -322,10 +365,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 @@ -358,9 +399,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) { @@ -382,9 +421,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 @@ -397,7 +434,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 71% 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 715092a756..77cd727c48 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShellCommand.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt @@ -8,11 +8,8 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 @@ -22,6 +19,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..5f847b37d9 --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsWithContext.kt @@ -0,0 +1,31 @@ +/* + * 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 +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..a6c51c8047 --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt @@ -0,0 +1,120 @@ +/* + * 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 +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..b1eb0fe450 --- /dev/null +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt @@ -0,0 +1,236 @@ +/* + * 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 +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 2b06fc7c13..4c44858584 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 @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 95% 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 bf7475ebc4..05b281fa6c 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 @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 6f08848cf6..819e1913bb 100644 --- a/node/src/test/kotlin/net/corda/node/shell/CustomTypeJsonParsingTests.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -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 82% 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 9bcb383313..0c85d67be7 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -8,7 +8,7 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -package net.corda.node +package net.corda.tools.shell import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import net.corda.client.jackson.JacksonSupport @@ -21,15 +21,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.* @@ -40,16 +34,6 @@ class InteractiveShellTest { private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) } - @Before - fun setup() { - InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock()) - } - - @After - fun shutdown() { - InteractiveShell.database.close() - } - @Suppress("UNUSED") class FlowA(val a: String) : FlowLogic() { constructor(b: Int?) : this(b.toString()) @@ -62,7 +46,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..39f18b02b4 --- /dev/null +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -0,0 +1,214 @@ +/* + * 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 +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