Merge pull request #526 from corda/merges/CORDA-792

Merge up from OS master including Standalone Shell and changes..
This commit is contained in:
szymonsztuka 2018-03-07 19:02:25 +00:00 committed by GitHub
commit b106af63eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1409 additions and 310 deletions

View File

@ -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 getServiceFlows()
@org.jetbrains.annotations.NotNull public abstract List getServices() @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 <init>(String, Throwable)
##
public final class net.corda.core.cordapp.CordappContext extends java.lang.Object public final class net.corda.core.cordapp.CordappContext extends java.lang.Object
public <init>(net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig) public <init>(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.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId()
@org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() @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() @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp()
## ##
@net.corda.core.DoNotImplement public interface net.corda.core.cordapp.CordappProvider @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.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)
@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.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 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.ContractUpgradeService getContractUpgradeService()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.KeyManagementService getKeyManagementService() @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 @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization
public abstract int version() 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 @net.corda.core.serialization.CordaSerializable public final class net.corda.core.serialization.MissingAttachmentsException extends net.corda.core.CordaException
public <init>(List) public <init>(List)
@org.jetbrains.annotations.NotNull public final List getIds() @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 @net.corda.core.DoNotImplement public interface net.corda.core.serialization.SerializationContext
@org.jetbrains.annotations.NotNull public abstract ClassLoader getDeserializationClassLoader() @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() public abstract boolean getObjectReferencesEnabled()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion() @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion()
@org.jetbrains.annotations.NotNull public abstract Map getProperties() @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.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 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 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 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 withProperty(Object, Object)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withWhitelisted(Class) @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() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getSTORAGE_CONTEXT()
public static final net.corda.core.serialization.SerializationDefaults INSTANCE 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 abstract class net.corda.core.serialization.SerializationFactory extends java.lang.Object
public <init>() public <init>()
public final Object asCurrent(kotlin.jvm.functions.Function1) 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 @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) 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 net.corda.core.utilities.ByteSequence copy()
@org.jetbrains.annotations.NotNull public final byte[] copyBytes()
public boolean equals(Object) public boolean equals(Object)
@org.jetbrains.annotations.NotNull public abstract byte[] getBytes() @org.jetbrains.annotations.NotNull public abstract byte[] getBytes()
public final int getOffset() 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)
@kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, 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.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 subSequence(int, int)
@org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int)
@org.jetbrains.annotations.NotNull public String toString() @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 net.corda.core.utilities.ByteSequence$Companion Companion
## ##
public static final class net.corda.core.utilities.ByteSequence$Companion extends java.lang.Object 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.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)
@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.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 final net.corda.testing.services.MockAttachmentStorage getAttachments()
@org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public java.time.Clock getClock()
@org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @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.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)
@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.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 net.corda.core.node.services.AttachmentStorage getAttachments()
@org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public java.time.Clock getClock()
@org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService()

3
.idea/compiler.xml generated
View File

@ -135,6 +135,9 @@
<module name="sgx-hsm-tool_test" target="1.8" /> <module name="sgx-hsm-tool_test" target="1.8" />
<module name="sgx-jvm_hsm-tool_main" target="1.8" /> <module name="sgx-jvm_hsm-tool_main" target="1.8" />
<module name="sgx-jvm_hsm-tool_test" target="1.8" /> <module name="sgx-jvm_hsm-tool_test" target="1.8" />
<module name="shell_integrationTest" target="1.8" />
<module name="shell_main" target="1.8" />
<module name="shell_test" target="1.8" />
<module name="simm-valuation-demo_integrationTest" target="1.8" /> <module name="simm-valuation-demo_integrationTest" target="1.8" />
<module name="simm-valuation-demo_main" target="1.8" /> <module name="simm-valuation-demo_main" target="1.8" />
<module name="simm-valuation-demo_test" target="1.8" /> <module name="simm-valuation-demo_test" target="1.8" />

View File

@ -11,6 +11,7 @@ changes to this list.
* Andras Slemmer (R3) * Andras Slemmer (R3)
* Andrius Dagys (R3) * Andrius Dagys (R3)
* Andrzej Cichocki (R3) * Andrzej Cichocki (R3)
* Andrzej Grzesik (R3)
* Anthony Coates (Deutsche Bank) * Anthony Coates (Deutsche Bank)
* Anton Semenov (Commerzbank) * Anton Semenov (Commerzbank)
* Antonio Cerrato (SEB) * Antonio Cerrato (SEB)
@ -92,7 +93,7 @@ changes to this list.
* Matthijs van den Bos (ING) * Matthijs van den Bos (ING)
* Michal Kit (R3) * Michal Kit (R3)
* Micheal Hinstridge (Thoughtworks) * Micheal Hinstridge (Thoughtworks)
* Michelle Sollecito (R3) * Michele Sollecito (R3)
* Mike Hearn (R3) * Mike Hearn (R3)
* Mike Reichelt (US Bank) * Mike Reichelt (US Bank)
* Mustafa Ozturk (Natixis) * Mustafa Ozturk (Natixis)

View File

@ -90,6 +90,7 @@ buildscript {
ext.ghostdriver_version = '2.1.0' ext.ghostdriver_version = '2.1.0'
ext.eaagentloader_version = '1.0.3' ext.eaagentloader_version = '1.0.3'
ext.curator_version = '4.0.0' 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: // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131' ext.java8_minUpdateVersion = '131'

View File

@ -83,7 +83,8 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration)
class CordaRPCClient private constructor( class CordaRPCClient private constructor(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
sslConfiguration: SSLConfiguration? = null sslConfiguration: SSLConfiguration? = null,
classLoader: ClassLoader? = null
) { ) {
@JvmOverloads @JvmOverloads
constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(hostAndPort, configuration, null) constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(hostAndPort, configuration, null)
@ -96,6 +97,15 @@ class CordaRPCClient private constructor(
): CordaRPCClient { ): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, sslConfiguration) 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 { init {
@ -103,7 +113,7 @@ class CordaRPCClient private constructor(
effectiveSerializationEnv effectiveSerializationEnv
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
try { try {
KryoClientSerializationScheme.initialiseSerialization() KryoClientSerializationScheme.initialiseSerialization(classLoader)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// Race e.g. two of these constructed in parallel, ignore. // Race e.g. two of these constructed in parallel, ignore.
} }
@ -113,7 +123,7 @@ class CordaRPCClient private constructor(
private val rpcClient = RPCClient<CordaRPCOps>( private val rpcClient = RPCClient<CordaRPCOps>(
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration), tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration),
configuration.toRpcClientConfiguration(), configuration.toRpcClientConfiguration(),
KRYO_RPC_CLIENT_CONTEXT if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT
) )
/** /**

View File

@ -21,3 +21,10 @@ fun createCordaRPCClientWithSsl(
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
sslConfiguration: SSLConfiguration? = null sslConfiguration: SSLConfiguration? = null
) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration) ) = 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)

View File

@ -43,18 +43,19 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
companion object { companion object {
/** Call from main only. */ /** Call from main only. */
fun initialiseSerialization() { fun initialiseSerialization(classLoader: ClassLoader? = null) {
nodeSerializationEnv = createSerializationEnv() nodeSerializationEnv = createSerializationEnv(classLoader)
} }
fun createSerializationEnv(): SerializationEnvironment { fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment {
return SerializationEnvironmentImpl( return SerializationEnvironmentImpl(
SerializationFactoryImpl().apply { SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme()) registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme(emptyList())) registerScheme(AMQPClientSerializationScheme(emptyList()))
}, },
AMQP_P2P_CONTEXT, if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT,
rpcClientContext = KRYO_RPC_CLIENT_CONTEXT) rpcClientContext = if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT)
} }
} }
} }

View File

@ -78,16 +78,16 @@ public/private keypairs and certificates. The keypairs and certificates should o
Certificate role extension Certificate role extension
-------------------------- --------------------------
Corda certificates have a custom X.509 v3 extension that specifies the role the certificate relates to. This 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: The extension contains a single ASN.1 integer identifying the identity type the certificate is for:
1. Doorman 1. Doorman
2. Network map 2. Network map
3. Service identity (such as a notary or oracle) 3. Service identity (currently only used as the shared identity in distributed notaries)
3. Node certificate authority (from which the TLS and well-known identity certificates are issued) 4. Node certificate authority (from which the TLS and well-known identity certificates are issued)
4. Transport layer security 5. Transport layer security
5. Well-known legal identity 6. Well-known legal identity
6. Confidential legal identity 7. Confidential legal identity
In a typical installation, node administrators needn't be aware of these. However, when node certificates are managed 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 by external tools (such as an existing PKI solution deployed within an organisation), it is important to understand

View File

@ -9,7 +9,7 @@ Shell
.. contents:: .. 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: the `CRaSH`_ shell and supports many of the same features. These features include:
* Invoking any of the node's RPC methods * 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 * 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 * 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 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 In development mode, the shell will display in the node's terminal window.
``--no-local-shell`` flag when running the node. 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: .. _ssh_server:
@ -44,8 +55,8 @@ By default, the SSH server is *disabled*. To enable it, a port must be configure
Authentication Authentication
************** **************
Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates Users log in to shell via SSH using the same credentials as for RPC.
with the node using RPC calls. No RPC permissions are required to allow the connection and log in. No RPC permissions are required to allow the connection and log in.
The host key is loaded from the ``<node root directory>/sshkey/hostkey.pem`` file. If this file does not exist, it is The host key is loaded from the ``<node root directory>/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 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. 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 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! ``-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. 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 Interacting with the node via the shell
--------------------------------------- ---------------------------------------

View File

@ -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

View File

@ -13,6 +13,7 @@ package net.corda.nodeapi.internal.serialization.amqp
import com.google.common.primitives.Primitives import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import org.apache.qpid.proton.codec.Data 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.isAccessible
import kotlin.reflect.jvm.javaType 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. * Code for finding the constructor we will use for deserialization.
* *

View File

@ -13,6 +13,7 @@ package net.corda.nodeapi.internal.serialization.amqp;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.ContractState; import net.corda.core.contracts.ContractState;
import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AbstractParty;
import net.corda.core.serialization.ConstructorForDeserialization;
import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.nodeapi.internal.serialization.AllWhitelist;
import net.corda.core.serialization.SerializedBytes; import net.corda.core.serialization.SerializedBytes;
import org.apache.qpid.proton.codec.DecoderImpl; import org.apache.qpid.proton.codec.DecoderImpl;

View File

@ -15,6 +15,7 @@ import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sign import net.corda.core.crypto.sign
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.common.internal.ProjectStructure.projectRootDir

View File

@ -12,8 +12,7 @@ package net.corda.nodeapi.internal.serialization.amqp
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import org.slf4j.Logger import net.corda.core.serialization.ConstructorForDeserialization
import org.slf4j.LoggerFactory
import org.junit.Test import org.junit.Test
import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.amqp.Symbol
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions

View File

@ -10,6 +10,7 @@
package net.corda.nodeapi.internal.serialization.amqp package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.ConstructorForDeserialization
import org.junit.Test import org.junit.Test
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.test.assertEquals import kotlin.test.assertEquals

View File

@ -76,6 +76,7 @@ dependencies {
compile project(':node-api') compile project(':node-api')
compile project(":confidential-identities") compile project(":confidential-identities")
compile project(':client:rpc') compile project(':client:rpc')
compile project(':tools:shell')
compile "net.corda.plugins:cordform-common:$gradle_plugins_version" compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// Log4J: logging framework (with SLF4J bindings) // Log4J: logging framework (with SLF4J bindings)
@ -113,10 +114,6 @@ dependencies {
exclude group: "asm" 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 // Coda Hale's Metrics: for monitoring of key statistics
compile "io.dropwizard.metrics:metrics-core:3.1.2" compile "io.dropwizard.metrics:metrics-core:3.1.2"
compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.1.2' compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.1.2'
@ -163,17 +160,6 @@ dependencies {
// Netty: All of it. // Netty: All of it.
compile "io.netty:netty-all:$netty_version" 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. // OkHTTP: Simple HTTP library.
compile "com.squareup.okhttp3:okhttp:$okhttp_version" compile "com.squareup.okhttp3:okhttp:$okhttp_version"
@ -191,9 +177,6 @@ dependencies {
integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "junit:junit:$junit_version"
integrationTestCompile "org.assertj:assertj-core:${assertj_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 // AgentLoader: dynamic loading of JVM agents
compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}" compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}"

View File

@ -15,12 +15,12 @@ import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions.Companion.all 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.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_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.DriverParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree

View File

@ -52,6 +52,7 @@ import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.api.* import net.corda.node.services.api.*
import net.corda.node.services.config.* 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.NodeSchedulerService
import net.corda.node.services.events.ScheduledActivityObserver import net.corda.node.services.events.ScheduledActivityObserver
import net.corda.node.services.identity.PersistentIdentityService 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.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.services.vault.VaultSoftLockManager
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.JVMAgentRegistry import net.corda.node.utilities.JVMAgentRegistry
import net.corda.node.utilities.NodeBuildProperties 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.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.storeLegalIdentity import net.corda.nodeapi.internal.storeLegalIdentity
import net.corda.tools.shell.InteractiveShell
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
import org.slf4j.Logger import org.slf4j.Logger
@ -269,7 +270,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
tokenizableServices = nodeServices + cordaServices + schedulerService tokenizableServices = nodeServices + cordaServices + schedulerService
registerCordappFlows(smm) registerCordappFlows(smm)
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } _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) Pair(StartedNodeImpl(this@AbstractNode, _services, nodeInfo, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
} }
networkMapUpdater = NetworkMapUpdater(services.networkMapCache, networkMapUpdater = NetworkMapUpdater(services.networkMapCache,
@ -307,9 +308,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
*/ */
protected abstract fun getRxIoScheduler(): Scheduler protected abstract fun getRxIoScheduler(): Scheduler
open fun startShell(rpcOps: CordaRPCOps) { open fun startShell() {
if (configuration.shouldInitCrashShell()) { 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())
} }
} }

View File

@ -11,6 +11,7 @@
package net.corda.node.internal package net.corda.node.internal
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import net.corda.client.rpc.internal.KryoClientSerializationScheme
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.thenMatch 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.serialization.KryoServerSerializationScheme
import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.NodePropertiesStore
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.*
import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.shell.shellUser
import net.corda.node.services.config.VerifierType
import net.corda.node.services.messaging.* import net.corda.node.services.messaging.*
import net.corda.node.services.rpc.ArtemisRpcBroker import net.corda.node.services.rpc.ArtemisRpcBroker
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
@ -170,7 +170,7 @@ open class Node(configuration: NodeConfiguration,
val securityManagerConfig = configuration.security?.authService ?: val securityManagerConfig = configuration.security?.authService ?:
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) 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 serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker(networkParameters)
val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) { val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) {
@ -414,11 +414,13 @@ open class Node(configuration: NodeConfiguration,
SerializationFactoryImpl().apply { SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme()) registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps))
registerScheme(KryoClientSerializationScheme())
}, },
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
rpcServerContext = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader), rpcServerContext = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader),
storageContext = AMQP_STORAGE_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 private var rpcMessagingClient: RPCMessagingClient? = null

View File

@ -22,12 +22,13 @@ import net.corda.node.*
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.shouldStartLocalShell 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.services.transactions.bftSMaRtSerialFilter
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.tools.shell.InteractiveShell
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole import org.fusesource.jansi.AnsiConsole
@ -153,7 +154,6 @@ open class NodeStartup(val args: Array<String>) {
node.generateAndSaveNodeInfo() node.generateAndSaveNodeInfo()
return return
} }
val startedNode = node.start() val startedNode = node.start()
Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name }) Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name })
startedNode.internals.nodeReadyFuture.thenMatch({ startedNode.internals.nodeReadyFuture.thenMatch({
@ -165,12 +165,15 @@ open class NodeStartup(val args: Array<String>) {
if (conf.shouldStartLocalShell()) { if (conf.shouldStartLocalShell()) {
startedNode.internals.startupComplete.then { startedNode.internals.startupComplete.then {
try { try {
InteractiveShell.runLocalShell(startedNode) InteractiveShell.runLocalShell( {startedNode.dispose()} )
} catch (e: Throwable) { } catch (e: Throwable) {
logger.error("Shell failed to start", e) logger.error("Shell failed to start", e)
} }
} }
} }
if (conf.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
}
}, },
{ th -> { th ->
logger.error("Unexpected exception during registration", th) logger.error("Unexpected exception during registration", th)

View File

@ -25,6 +25,7 @@ import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration 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 // Supported types of authentication/authorization data providers
enum class AuthDataSourceType { enum class AuthDataSourceType {
// External RDBMS // 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 // Optional components: cache
data class Options(val cache: Options.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) 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 { companion object {

View File

@ -15,7 +15,6 @@ import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
data class SslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration { 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) fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(), keyStorePassword: String = this.keyStorePassword, trustStorePassword: String = this.trustStorePassword): SslOptions = copy(certificatesDirectory = certificatesDirectory.toAbsolutePath(), keyStorePassword = keyStorePassword, trustStorePassword = trustStorePassword)
} }

View File

@ -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() }

View File

@ -103,7 +103,7 @@ class NodeAttachmentService(
@ElementCollection @ElementCollection
@Column(name = "contract_class_name") @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")), JoinColumn(name = "att_id", referencedColumnName = "att_id")),
foreignKey = ForeignKey(name = "FK__ctr_class__attachments")) foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null var contractClassNames: List<ContractClassName>? = null

View File

@ -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<T>(val invocationContext: InvocationContext, val authorizingSubject: AuthorizingSubject, val block:() -> T): Thread() {
private var result: CompletableFuture<T> = 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<T> {
start()
join()
return result
}
}

View File

@ -13,6 +13,7 @@ package net.corda.node.services.config
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.tools.shell.SSHDConfiguration
import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
@ -86,7 +87,7 @@ class NodeConfigurationImplTest {
return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions) return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions)
} }
private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl { private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl {
return testConfiguration.copy(dataSourceProperties = dataSourceProperties) return testConfiguration.copy(dataSourceProperties = dataSourceProperties)
} }

View File

@ -21,12 +21,12 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.all
import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.messaging.RPCMessagingClient 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.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User 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.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree

View File

@ -56,6 +56,7 @@ include 'tools:graphs'
include 'tools:bootstrapper' include 'tools:bootstrapper'
include 'tools:dbmigration' include 'tools:dbmigration'
include 'tools:notaryhealthcheck' include 'tools:notaryhealthcheck'
include 'tools:shell'
include 'example-code' include 'example-code'
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code") project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
include 'samples:attachment-demo' include 'samples:attachment-demo'

View File

@ -288,7 +288,7 @@ open class InternalMockNetwork(private val cordappPackages: List<String>,
return E2ETestKeyManagementService(identityService, keyPairs) return E2ETestKeyManagementService(identityService, keyPairs)
} }
override fun startShell(rpcOps: CordaRPCOps) { override fun startShell() {
//No mock shell //No mock shell
} }

View File

@ -8,11 +8,11 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.identity.CordaX500Name
import net.corda.core.internal.div 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 net.corda.nodeapi.internal.crypto.*
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import sun.security.tools.keytool.CertAndKeyGen 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 { interface AutoClosableSSLConfiguration : AutoCloseable {
val value: SslOptions val value: SSLConfiguration
} }
typealias KeyStoreEntry = Pair<String, UnsafeCertificate> typealias KeyStoreEntry = Pair<String, UnsafeCertificate>
@ -192,7 +193,7 @@ private fun newKeyStore(type: String, password: String): KeyStore {
return 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) val serverDir = Files.createTempDirectory(null)
FileUtils.forceDeleteOnExit(serverDir.toFile()) FileUtils.forceDeleteOnExit(serverDir.toFile())

View File

@ -19,7 +19,6 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.SslOptions
import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevNodeCa 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. */ /** 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 doLookup(map: Map<*, *>, argIndex: Int = 0) = doAnswer { map[it.arguments[argIndex]] }
fun SslOptions.useSslRpcOverrides(): Map<String, String> { fun SSLConfiguration.useSslRpcOverrides(): Map<String, String> {
return mapOf( return mapOf(
"rpcSettings.useSsl" to "true", "rpcSettings.useSsl" to "true",
"rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), "rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(),
@ -140,7 +139,7 @@ fun SslOptions.useSslRpcOverrides(): Map<String, String> {
) )
} }
fun SslOptions.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map<String, String> { fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map<String, String> {
return mapOf( return mapOf(
"rpcSettings.adminAddress" to rpcAdminAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(),
"rpcSettings.useSsl" to "false", "rpcSettings.useSsl" to "false",

92
tools/shell/build.gradle Normal file
View File

@ -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
}

View File

@ -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<SSHServerTest.FlowICanRun>(),
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<SSHServerTest.FlowICanRun>(),
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()
}
}
}
}

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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 co.paralleluniverse.fibers.Suspendable
import com.jcraft.jsch.ChannelExec 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.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap 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.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_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.ClassRule
import org.junit.Test import org.junit.Test
import java.net.ConnectException import java.net.ConnectException
import java.util.regex.Pattern
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
@ -110,7 +111,8 @@ class SSHServerTest : IntegrationTest() {
@Test @Test
fun `ssh respects permissions`() { fun `ssh respects permissions`() {
val user = User("u", "p", setOf(startFlow<FlowICanRun>())) val user = User("u", "p", setOf(startFlow<FlowICanRun>(),
invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name)))
// The driver will automatically pick up the annotated flows below // The driver will automatically pick up the annotated flows below
driver(DriverParameters(isDebug = true)) { driver(DriverParameters(isDebug = true)) {
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
@ -125,12 +127,10 @@ class SSHServerTest : IntegrationTest() {
assertTrue(session.isConnected) assertTrue(session.isConnected)
val channel = session.openChannel("exec") as ChannelExec val channel = session.openChannel("exec") as ChannelExec
channel.setCommand("start FlowICannotRun otherParty: \"${ALICE_NAME}\"") channel.setCommand("start FlowICannotRun otherParty: \"$ALICE_NAME\"")
channel.connect() channel.connect()
val response = String(Streams.readAll(channel.inputStream)) val response = String(Streams.readAll(channel.inputStream))
val flowNameEscaped = Pattern.quote("StartFlow.${SSHServerTest::class.qualifiedName}$${FlowICannotRun::class.simpleName}")
channel.disconnect() channel.disconnect()
session.disconnect() session.disconnect()
@ -156,11 +156,17 @@ class SSHServerTest : IntegrationTest() {
val channel = session.openChannel("exec") as ChannelExec val channel = session.openChannel("exec") as ChannelExec
channel.setCommand("start FlowICanRun") channel.setCommand("start FlowICanRun")
channel.connect() channel.connect(5000)
assertTrue(channel.isConnected)
val response = String(Streams.readAll(channel.inputStream)) val response = String(Streams.readAll(channel.inputStream))
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") } 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. // There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
assertThat(linesWithDoneCount).size().isGreaterThanOrEqualTo(1) assertThat(linesWithDoneCount).size().isGreaterThanOrEqualTo(1)
} }

View File

@ -0,0 +1,8 @@
user=demo1
baseDirectory="/Users/szymonsztuka/Documents/shell-config"
hostAndPort="localhost:10006"
sshdPort=2223
ssl {
keyStorePassword=password
trustStorePassword=password
}

View File

@ -8,13 +8,14 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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 // See the comments at the top of run.java
import com.fasterxml.jackson.databind.ObjectMapper;
import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.CordaRPCOps;
import net.corda.node.utilities.ANSIProgressRenderer; import net.corda.tools.shell.utlities.ANSIProgressRenderer;
import net.corda.node.utilities.CRaSHANSIProgressRenderer; import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer;
import org.crsh.cli.*; import org.crsh.cli.*;
import org.crsh.command.*; import org.crsh.command.*;
import org.crsh.text.*; import org.crsh.text.*;
@ -22,7 +23,8 @@ import org.crsh.text.ui.TableElement;
import java.util.*; 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( @Man(
"Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + "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 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<String> input @Usage("The data to pass as input") @Argument(unquote = false) List<String> input
) { ) {
startFlow(name, input, out, ops(), ansiProgressRenderer()); startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper());
} }
// TODO Limit number of flows shown option? // 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<String> input, @Usage("The data to pass as input") @Argument(unquote = false) List<String> input,
RenderPrintWriter out, RenderPrintWriter out,
CordaRPCOps rpcOps, CordaRPCOps rpcOps,
ANSIProgressRenderer ansiProgressRenderer) { ANSIProgressRenderer ansiProgressRenderer,
ObjectMapper om) {
if (name == null) { if (name == null) {
out.println("You must pass a name for the flow, see 'man flow'", Color.red); out.println("You must pass a name for the flow, see 'man flow'", Color.red);
return; return;
} }
String inp = input == null ? "" : String.join(" ", input).trim(); 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 @Command

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.core.messaging.*;
import net.corda.client.jackson.*; import net.corda.client.jackson.*;
@ -40,7 +40,7 @@ public class RunShellCommand extends InteractiveShellCommand {
return null; return null;
} }
return InteractiveShell.runRPCFromString(command, out, context, ops()); return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper());
} }
private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) { private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) {

View File

@ -8,12 +8,12 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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. // A simple forwarder to the "flow start" command, for easier typing.
import net.corda.node.utilities.ANSIProgressRenderer; import net.corda.tools.shell.utlities.ANSIProgressRenderer;
import net.corda.node.utilities.CRaSHANSIProgressRenderer; import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer;
import org.crsh.cli.*; import org.crsh.cli.*;
import java.util.*; 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, 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<String> input) { @Usage("The data to pass as input") @Argument(unquote = false) List<String> input) {
ANSIProgressRenderer ansiProgressRenderer = ansiProgressRenderer(); 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());
} }
} }

View File

@ -8,20 +8,20 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.core.messaging.CordaRPCOps
import net.corda.node.internal.security.Password import net.corda.core.utilities.loggerFor
import net.corda.node.internal.security.RPCSecurityManager import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import net.corda.node.internal.security.tryAuthenticate
import org.crsh.auth.AuthInfo import org.crsh.auth.AuthInfo
import org.crsh.auth.AuthenticationPlugin import org.crsh.auth.AuthenticationPlugin
import org.crsh.plugin.CRaSHPlugin import org.crsh.plugin.CRaSHPlugin
class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> { class CordaAuthenticationPlugin(private val rpcOps: (username: String, credential: String) -> CordaRPCOps): CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
companion object {
private val logger = loggerFor<CordaAuthenticationPlugin>()
}
override fun getImplementation(): AuthenticationPlugin<String> = this override fun getImplementation(): AuthenticationPlugin<String> = this
@ -32,10 +32,13 @@ class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val sec
if (username == null || credential == null) { if (username == null || credential == null) {
return AuthInfo.UNSUCCESSFUL return AuthInfo.UNSUCCESSFUL
} }
val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential)) try {
if (authorizingSubject != null) { val ops = rpcOps(username, credential)
val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName) return CordaSSHAuthInfo(true, ops)
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject)) } catch (e: ActiveMQSecurityException) {
logger.warn(e.message)
} catch (e: Exception) {
logger.warn(e.message, e)
} }
return AuthInfo.UNSUCCESSFUL return AuthInfo.UNSUCCESSFUL
} }

View File

@ -8,12 +8,18 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.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 import org.crsh.auth.AuthInfo
class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null) : AuthInfo { class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null) : AuthInfo {
override fun isSuccessful(): Boolean = successful override fun isSuccessful(): Boolean = successful
val yamlInputMapper: ObjectMapper by lazy {
createYamlInputMapper(rpcOps)
}
} }

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.flows.StateMachineRunId
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.JsonGenerator
import com.fasterxml.jackson.core.JsonParser 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.JacksonSupport
import net.corda.client.jackson.StringToMethodCallParser import net.corda.client.jackson.StringToMethodCallParser
import net.corda.client.rpc.PermissionException import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.UniqueIdentifier import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.internal.concurrent.doneFuture 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.FlowProgressHandle
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.Node import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.node.internal.StartedNode import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.node.internal.security.AdminSubject import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
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 org.crsh.command.InvocationContext import org.crsh.command.InvocationContext
import org.crsh.console.jline.JLineProcessor import org.crsh.console.jline.JLineProcessor
import org.crsh.console.jline.TerminalFactory import org.crsh.console.jline.TerminalFactory
@ -70,6 +62,7 @@ import rx.Observable
import rx.Subscriber import rx.Subscriber
import java.io.* import java.io.*
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.UndeclaredThrowableException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -90,61 +83,98 @@ import kotlin.concurrent.thread
// TODO: Resurrect or reimplement the mail plugin. // TODO: Resurrect or reimplement the mail plugin.
// TODO: Make it notice new shell commands added after the node started. // 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 { object InteractiveShell {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
private lateinit var node: StartedNode<Node> private lateinit var rpcOps: (username: String, credentials: String) -> CordaRPCOps
@VisibleForTesting private lateinit var connection: CordaRPCOps
internal lateinit var database: CordaPersistence
private lateinit var rpcOps: CordaRPCOps
private lateinit var securityManager: RPCSecurityManager
private lateinit var identityService: IdentityService
private var shell: Shell? = null 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 * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
* internals. * internals.
*/ */
fun startShell(configuration: NodeConfiguration, cordaRPCOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) { fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
this.rpcOps = cordaRPCOps rpcOps = { username: String, credentials: String ->
this.securityManager = securityManager val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort,
this.identityService = identityService sslConfiguration = configuration.ssl, classLoader = classLoader)
this.nodeLegalName = configuration.myLegalName client.start(username, credentials).proxy
this.database = database }
val dir = configuration.baseDirectory InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshd != null val runSshDaemon = configuration.sshdPort != null
val config = Properties() val config = Properties()
if (runSshDaemon) { 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. // 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.port"] = configuration.sshdPort?.toString()
config["crash.ssh.keygen"] = "true"
config["crash.ssh.port"] = configuration.sshd?.port.toString()
config["crash.auth"] = "corda" 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("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("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) ExternalResolver.INSTANCE.addCommand("start", "An alias for 'flow start'", StartShellCommand::class.java)
shell = ShellLifecycle(dir).start(config) shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
if (runSshDaemon) {
Node.printBasicNodeInfo("SSH server listening on port", configuration.sshd!!.port.toString())
}
} }
fun runLocalShell(node: StartedNode<Node>) { fun runLocalShell(onExit: () -> Unit = {}) {
val terminal = TerminalFactory.create() val terminal = TerminalFactory.create()
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
InterruptHandler { jlineProcessor.interrupt() }.install() InterruptHandler { jlineProcessor.interrupt() }.install()
thread(name = "Command line shell processor", isDaemon = true) { 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 { Emoji.renderIfSupported {
jlineProcessor.run() jlineProcessor.run()
} }
@ -154,22 +184,22 @@ object InteractiveShell {
jlineProcessor.closed() jlineProcessor.closed()
log.info("Command shell has exited") log.info("Command shell has exited")
terminal.restore() terminal.restore()
node.dispose() onExit.invoke()
} }
} }
class ShellLifecycle(val dir: Path) : PluginLifeCycle() { class ShellLifecycle(private val shellCommands: Path) : PluginLifeCycle() {
fun start(config: Properties): Shell { fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell {
val classLoader = this.javaClass.classLoader val classLoader = this.javaClass.classLoader
val classpathDriver = ClassPathMountFactory(classLoader) val classpathDriver = ClassPathMountFactory(classLoader)
val fileDriver = FileMountFactory(Utils.getCurrentDirectory()) val fileDriver = FileMountFactory(Utils.getCurrentDirectory())
val extraCommandsPath = (dir / "shell-commands").toAbsolutePath().createDirectories() val extraCommandsPath = shellCommands.toAbsolutePath().createDirectories()
val commandsFS = FS.Builder() val commandsFS = FS.Builder()
.register("file", fileDriver) .register("file", fileDriver)
.mount("file:" + extraCommandsPath) .mount("file:" + extraCommandsPath)
.register("classpath", classpathDriver) .register("classpath", classpathDriver)
.mount("classpath:/net/corda/node/shell/") .mount("classpath:/net/corda/tools/shell/")
.mount("classpath:/crash/commands/") .mount("classpath:/crash/commands/")
.build() .build()
val confFS = FS.Builder() 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 // 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 // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
// is only the 'jmx' command. // 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( val attributes = emptyMap<String,Any>()
"ops" to rpcOps,
"mapper" to yamlInputMapper
)
val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader) val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader)
context.refresh() context.refresh()
this.config = config this.config = config
start(context) 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 // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra
// serializers. // serializers.
JacksonSupport.createInMemoryMapper(identityService, YAMLFactory(), true).apply { return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply {
val rpcModule = SimpleModule() val rpcModule = SimpleModule()
rpcModule.addDeserializer(InputStream::class.java, InputStreamDeserializer) rpcModule.addDeserializer(InputStream::class.java, InputStreamDeserializer)
rpcModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) 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. * the [runFlowFromString] method and starts the requested flow. Ctrl-C can be used to cancel.
*/ */
@JvmStatic @JvmStatic
fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer) { fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer, om: ObjectMapper) {
val matches = rpcOps.registeredFlows().filter { nameFragment in it } val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
return
}
if (matches.isEmpty()) { if (matches.isEmpty()) {
output.println("No matching flow found, run 'flow list' to see your options.", Color.red) output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
return return
@ -265,11 +304,15 @@ object InteractiveShell {
return return
} }
val clazz: Class<FlowLogic<*>> = uncheckedCast(Class.forName(matches.single())) val flowClazz: Class<FlowLogic<*>> = if (classLoader != null) {
uncheckedCast(Class.forName(matches.single(), true, classLoader))
} else {
uncheckedCast(Class.forName(matches.single()))
}
try { try {
// Show the progress tracker on the console until the flow completes or is interrupted with a // Show the progress tracker on the console until the flow completes or is interrupted with a
// Ctrl-C keypress. // 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) val latch = CountDownLatch(1)
ansiProgressRenderer.render(stateObservable, { latch.countDown() }) ansiProgressRenderer.render(stateObservable, { latch.countDown() })
@ -308,7 +351,7 @@ object InteractiveShell {
fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>, fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>,
inputData: String, inputData: String,
clazz: Class<out FlowLogic<T>>, clazz: Class<out FlowLogic<T>>,
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> { om: ObjectMapper): FlowProgressHandle<T> {
// For each constructor, attempt to parse the input data as a method call. Use the first that succeeds, // 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. // and keep track of the reasons we failed so we can print them out if no constructors are usable.
val parser = StringToMethodCallParser(clazz, om) val parser = StringToMethodCallParser(clazz, om)
@ -322,10 +365,8 @@ object InteractiveShell {
try { try {
// Attempt construction with the given arguments. // Attempt construction with the given arguments.
val args = database.transaction { paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor)
paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) val args = parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.parameterTypes), inputData)
parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.parameterTypes), inputData)
}
if (args.size != ctor.parameterTypes.size) { if (args.size != ctor.parameterTypes.size) {
errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)") errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)")
continue continue
@ -358,9 +399,7 @@ object InteractiveShell {
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed() val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) } val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
val subscriber = FlowWatchPrintingSubscriber(out) val subscriber = FlowWatchPrintingSubscriber(out)
database.transaction { stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber)
stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber)
}
var result: Any? = subscriber.future var result: Any? = subscriber.future
if (result is Future<*>) { if (result is Future<*>) {
if (!result.isDone) { if (!result.isDone) {
@ -382,9 +421,7 @@ object InteractiveShell {
} }
@JvmStatic @JvmStatic
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps): Any? { fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper): Any? {
val parser = StringToMethodCallParser(CordaRPCOps::class.java, context.attributes["mapper"] as ObjectMapper)
val cmd = input.joinToString(" ").trim { it <= ' ' } val cmd = input.joinToString(" ").trim { it <= ' ' }
if (cmd.toLowerCase().startsWith("startflow")) { if (cmd.toLowerCase().startsWith("startflow")) {
// The flow command provides better support and startFlow requires special handling anyway due to // The flow command provides better support and startFlow requires special handling anyway due to
@ -397,7 +434,8 @@ object InteractiveShell {
var result: Any? = null var result: Any? = null
try { try {
InputStreamSerializer.invokeContext = context 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() result = call.call()
if (result != null && result !is kotlin.Unit && result !is Void) { if (result != null && result !is kotlin.Unit && result !is Void) {
result = printAndFollowRPCResponse(result, out) result = printAndFollowRPCResponse(result, out)

View File

@ -8,11 +8,8 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.command.BaseCommand
import org.crsh.shell.impl.command.CRaSHSession import org.crsh.shell.impl.command.CRaSHSession
@ -22,6 +19,5 @@ import org.crsh.shell.impl.command.CRaSHSession
open class InteractiveShellCommand : BaseCommand() { open class InteractiveShellCommand : BaseCommand() {
fun ops() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).rpcOps fun ops() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).rpcOps
fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer
fun services() = context.attributes["services"] as ServiceHubInternal fun objectMapper() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).yamlInputMapper
fun objectMapper() = context.attributes["mapper"] as ObjectMapper
} }

View File

@ -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
}

View File

@ -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<String>) {
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<URL> =
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)
}
}

View File

@ -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>(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<String, Any?>()
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<ShellConfigurationFile.ShellConfigFile>()
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 })
}
}
}

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.internal.Emoji
import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.FlowProgressHandle

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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 // Note that this file MUST be in a sub-directory called "base" relative to the path
// given in the configuration code in InteractiveShell. // given in the configuration code in InteractiveShell.

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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.JsonMappingException
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper

View File

@ -8,7 +8,7 @@
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. * 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 com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import net.corda.client.jackson.JacksonSupport 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.internal.concurrent.openFuture
import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.shell.InteractiveShell import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.node.internal.configureDatabase
import net.corda.testing.core.TestIdentity 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 org.junit.Test
import rx.Observable import rx.Observable
import java.util.* import java.util.*
@ -40,16 +34,6 @@ class InteractiveShellTest {
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) 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") @Suppress("UNUSED")
class FlowA(val a: String) : FlowLogic<String>() { class FlowA(val a: String) : FlowLogic<String>() {
constructor(b: Int?) : this(b.toString()) constructor(b: Int?) : this(b.toString())
@ -62,7 +46,7 @@ class InteractiveShellTest {
override fun call() = a 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 val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory())
private fun check(input: String, expected: String) { private fun check(input: String, expected: String) {

View File

@ -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<String>()
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)
}
}

View File

@ -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"
}
}