mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Merge pull request #526 from corda/merges/CORDA-792
Merge up from OS master including Standalone Shell and changes..
This commit is contained in:
commit
b106af63eb
@ -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
3
.idea/compiler.xml
generated
@ -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" />
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
@ -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
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}"
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
|
@ -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
92
tools/shell/build.gradle
Normal 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
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
8
tools/shell/src/integration-test/resources/ssl.conf
Normal file
8
tools/shell/src/integration-test/resources/ssl.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
user=demo1
|
||||||
|
baseDirectory="/Users/szymonsztuka/Documents/shell-config"
|
||||||
|
hostAndPort="localhost:10006"
|
||||||
|
sshdPort=2223
|
||||||
|
ssl {
|
||||||
|
keyStorePassword=password
|
||||||
|
trustStorePassword=password
|
||||||
|
}
|
@ -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
|
@ -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) {
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
@ -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)
|
@ -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
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -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.
|
@ -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
|
@ -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) {
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
34
tools/shell/src/test/resources/config.conf
Normal file
34
tools/shell/src/test/resources/config.conf
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user