Skip to content

Commit c966769

Browse files
committed
feat(corda-connector): scp jars to nodes #621
Implements transferring the cordapp jar files onto Corda nodes via SSH+SCP. It supports deploying to multiple nodes in a single request via an array of deployment configuration parameters that the caller can specify. Credentials are travelling on the wire in plain text right now which is of course unacceptable and a follow-up issue has been created to rectify this by adding keychain support or some other type of solution that does not make it necessary for the caller to send up SSH and RPC credentials with their deployment request. Another thing that we'll have to fix prior to GA is that right now there is no SSH host key verification. For development mode Corda nodes: There's possibility to declare which cordapp jar contains database migrations for H2 and the deployment endpoint can run these as well. The graceful shutdown method is implemented on our RPC connection class because this is not yet supported on the version of Corda that we are targeting with the connector (v4.5). The new test that verifies that all is working well is called deploy-cordapp-jars-to-nodes.test.ts and what it does is very similar to the older test called jvm-kotlin-spring-server.test.ts but this one does its invocations with a contract that has been built and deployed from scratch not like the old test which relied on the jars already being present in the AIO Corda container by default. The current commit is also tagged in the container registry as: hyperledger/cactus-connector-corda-server:2021-03-16-feat-621 Fixes #621 Signed-off-by: Peter Somogyvari <[email protected]>
1 parent f52c00e commit c966769

File tree

18 files changed

+1103
-71
lines changed

18 files changed

+1103
-71
lines changed

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/.openapi-generator/FILES

+3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ settings.gradle
44
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/api/ApiPluginLedgerConnectorCorda.kt
55
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/api/ApiPluginLedgerConnectorCordaService.kt
66
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/api/ApiUtil.kt
7+
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/CordaNodeSshCredentials.kt
8+
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/CordaRpcCredentials.kt
79
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/CordaX500Name.kt
10+
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/CordappDeploymentConfig.kt
811
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/CordappInfo.kt
912
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/DeployContractJarsBadRequestV1Response.kt
1013
src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/DeployContractJarsSuccessV1Response.kt

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ dependencies {
6262
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.1")
6363
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1")
6464

65+
implementation("com.hierynomus:sshj:0.31.0")
66+
6567
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
6668
testImplementation("org.springframework.boot:spring-boot-starter-test") {
6769
exclude(module = "junit")

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/impl/ApiPluginLedgerConnectorCordaServiceImpl.kt

+236-59
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl
2+
3+
import net.corda.core.utilities.loggerFor
4+
import net.schmizz.sshj.common.KeyType
5+
import net.schmizz.sshj.transport.verification.HostKeyVerifier
6+
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
7+
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts.KnownHostEntry
8+
import java.io.*
9+
import java.lang.RuntimeException
10+
import java.nio.charset.Charset
11+
import java.security.PublicKey
12+
import java.util.*
13+
14+
// TODO: Once we are able to support host key verification this can be either
15+
// deleted or just left alone if it actually ends up being used by the
16+
// fix that makes it so that we can do host key verification.
17+
class InMemoryHostKeyVerifier(inputStream: InputStream?, charset: Charset?) :
18+
HostKeyVerifier {
19+
private val entries: MutableList<KnownHostEntry> = ArrayList()
20+
21+
companion object {
22+
val logger = loggerFor<InMemoryHostKeyVerifier>()
23+
}
24+
25+
init {
26+
27+
// we construct the OpenSSHKnownHosts instance with a dummy file that does not exist because
28+
// that's the only way to trick it into doing nothing on the file system which is what we want
29+
// since this implementation is about providing an in-memory host key verification process...
30+
val nonExistentFilePath = UUID.randomUUID().toString()
31+
val hostsFile = File(nonExistentFilePath)
32+
val openSSHKnownHosts = OpenSSHKnownHosts(hostsFile)
33+
34+
// we just wanted an EntryFactory which we could not instantiate without instantiating the OpenSSHKnownHosts
35+
// class as well (which is a limitation of Kotlin compared to Java it seems).
36+
val entryFactory: OpenSSHKnownHosts.EntryFactory = openSSHKnownHosts.EntryFactory()
37+
val reader = BufferedReader(InputStreamReader(inputStream, charset))
38+
while (reader.ready()) {
39+
val line = reader.readLine()
40+
try {
41+
logger.debug("Parsing line {}", line)
42+
val entry = entryFactory.parseEntry(line)
43+
if (entry != null) {
44+
entries.add(entry)
45+
logger.debug("Added entry {}", entry)
46+
}
47+
} catch (e: Exception) {
48+
throw RuntimeException("Failed to init InMemoryHostKeyVerifier", e)
49+
}
50+
}
51+
logger.info("Parsing of host key entries successful.")
52+
}
53+
54+
override fun verify(hostname: String, port: Int, key: PublicKey): Boolean {
55+
logger.debug("Verifying {}:{} {}", hostname, port, key)
56+
val type = KeyType.fromKey(key)
57+
if (type === KeyType.UNKNOWN) {
58+
logger.debug("Rejecting key due to unknown key type {}", type)
59+
return false
60+
}
61+
for (e in entries) {
62+
try {
63+
if (e.appliesTo(type, hostname) && e.verify(key)) {
64+
logger.debug("Accepting key type {} for host {} with key of {}", type, hostname, key)
65+
return true
66+
}
67+
} catch (ioe: IOException) {
68+
throw RuntimeException("Crashed while attempting to verify key type $type for host $hostname ", ioe)
69+
}
70+
}
71+
logger.debug("Rejecting due to none of the {} entries being acceptable.", entries.size)
72+
return false
73+
}
74+
}

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/impl/NodeRPCConnection.kt

+48-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl
22

33

44
import net.corda.client.rpc.CordaRPCClient
5+
import net.corda.client.rpc.CordaRPCClientConfiguration
56
import net.corda.client.rpc.CordaRPCConnection
7+
import net.corda.client.rpc.GracefulReconnect
68
import net.corda.core.messaging.CordaRPCOps
9+
import net.corda.core.messaging.pendingFlowsCount
710
import net.corda.core.utilities.NetworkHostAndPort
11+
import net.corda.core.utilities.loggerFor
812
import org.springframework.beans.factory.annotation.Value
913
import org.springframework.stereotype.Component
1014
import javax.annotation.PostConstruct
@@ -15,6 +19,7 @@ import java.net.InetAddress
1519

1620
import org.springframework.boot.context.properties.ConfigurationProperties
1721
import org.springframework.validation.annotation.Validated
22+
import java.util.concurrent.CountDownLatch
1823
import javax.validation.constraints.NotEmpty
1924
import javax.validation.constraints.NotNull
2025

@@ -48,16 +53,57 @@ open class NodeRPCConnection(
4853
final lateinit var proxy: CordaRPCOps
4954
private set
5055

56+
companion object {
57+
val logger = loggerFor<NodeRPCConnection>();
58+
}
59+
5160
@PostConstruct
5261
fun initialiseNodeRPCConnection() {
5362
val rpcAddress = NetworkHostAndPort(host, rpcPort)
54-
val rpcClient = CordaRPCClient(rpcAddress)
55-
rpcConnection = rpcClient.start(username, password)
63+
val rpcClient = CordaRPCClient(haAddressPool = listOf(rpcAddress))
64+
65+
var numReconnects = 0
66+
val gracefulReconnect = GracefulReconnect(
67+
onDisconnect={ logger.info("GracefulReconnect:onDisconnect()")},
68+
onReconnect={ logger.info("GracefulReconnect:onReconnect() #${++numReconnects}")},
69+
maxAttempts = 30
70+
)
71+
72+
rpcConnection = rpcClient.start(username, password, gracefulReconnect = gracefulReconnect)
5673
proxy = rpcConnection.proxy
5774
}
5875

5976
@PreDestroy
6077
override fun close() {
6178
rpcConnection.notifyServerAndClose()
6279
}
80+
81+
fun gracefulShutdown() {
82+
logger.debug(("Beginning graceful shutdown..."))
83+
val latch = CountDownLatch(1)
84+
@Suppress("DEPRECATION")
85+
val subscription = proxy.pendingFlowsCount().updates
86+
.doAfterTerminate(latch::countDown)
87+
.subscribe(
88+
// For each update.
89+
{ (completed, total) -> logger.info("...remaining flows: $completed / $total") },
90+
// On error.
91+
{
92+
logger.error(it.message)
93+
throw it
94+
},
95+
// When completed.
96+
{
97+
// This will only show up in the standalone Shell, because the embedded one
98+
// is killed as part of a node's shutdown.
99+
logger.info("...done shutting down gracefully.")
100+
}
101+
)
102+
proxy.terminate(true)
103+
latch.await()
104+
logger.debug("Concluded graceful shutdown OK")
105+
// Unsubscribe or we hold up the shutdown
106+
subscription.unsubscribe()
107+
rpcConnection.forceClose()
108+
}
63109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.model
2+
3+
import java.util.Objects
4+
import com.fasterxml.jackson.annotation.JsonProperty
5+
import javax.validation.constraints.DecimalMax
6+
import javax.validation.constraints.DecimalMin
7+
import javax.validation.constraints.Max
8+
import javax.validation.constraints.Min
9+
import javax.validation.constraints.NotNull
10+
import javax.validation.constraints.Pattern
11+
import javax.validation.constraints.Size
12+
import javax.validation.Valid
13+
14+
/**
15+
*
16+
* @param hostKeyEntry
17+
* @param username
18+
* @param password
19+
* @param hostname
20+
* @param port
21+
*/
22+
data class CordaNodeSshCredentials(
23+
24+
@get:NotNull
25+
@get:Size(min=1,max=65535)
26+
@field:JsonProperty("hostKeyEntry") val hostKeyEntry: kotlin.String,
27+
28+
@get:NotNull
29+
@get:Size(min=1,max=32)
30+
@field:JsonProperty("username") val username: kotlin.String,
31+
32+
@get:NotNull
33+
@get:Size(min=1,max=4096)
34+
@field:JsonProperty("password") val password: kotlin.String,
35+
36+
@get:NotNull
37+
@get:Size(min=1,max=4096)
38+
@field:JsonProperty("hostname") val hostname: kotlin.String,
39+
40+
@get:NotNull
41+
@get:Min(1)
42+
@get:Max(65535)
43+
@field:JsonProperty("port") val port: kotlin.Int
44+
) {
45+
46+
}
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.model
2+
3+
import java.util.Objects
4+
import com.fasterxml.jackson.annotation.JsonProperty
5+
import javax.validation.constraints.DecimalMax
6+
import javax.validation.constraints.DecimalMin
7+
import javax.validation.constraints.Max
8+
import javax.validation.constraints.Min
9+
import javax.validation.constraints.NotNull
10+
import javax.validation.constraints.Pattern
11+
import javax.validation.constraints.Size
12+
import javax.validation.Valid
13+
14+
/**
15+
*
16+
* @param hostname
17+
* @param port
18+
* @param username
19+
* @param password
20+
*/
21+
data class CordaRpcCredentials(
22+
23+
@get:NotNull
24+
@get:Size(min=1,max=65535)
25+
@field:JsonProperty("hostname") val hostname: kotlin.String,
26+
27+
@get:NotNull
28+
@get:Min(1)
29+
@get:Max(65535)
30+
@field:JsonProperty("port") val port: kotlin.Int,
31+
32+
@get:NotNull
33+
@get:Size(min=1,max=1024)
34+
@field:JsonProperty("username") val username: kotlin.String,
35+
36+
@get:NotNull
37+
@get:Size(min=1,max=65535)
38+
@field:JsonProperty("password") val password: kotlin.String
39+
) {
40+
41+
}
42+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.model
2+
3+
import java.util.Objects
4+
import com.fasterxml.jackson.annotation.JsonProperty
5+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.CordaNodeSshCredentials
6+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.CordaRpcCredentials
7+
import javax.validation.constraints.DecimalMax
8+
import javax.validation.constraints.DecimalMin
9+
import javax.validation.constraints.Max
10+
import javax.validation.constraints.Min
11+
import javax.validation.constraints.NotNull
12+
import javax.validation.constraints.Pattern
13+
import javax.validation.constraints.Size
14+
import javax.validation.Valid
15+
16+
/**
17+
*
18+
* @param sshCredentials
19+
* @param rpcCredentials
20+
* @param cordaNodeStartCmd The shell command to execute in order to start back up a Corda node after having placed new jars in the cordapp directory of said node.
21+
* @param cordappDir The absolute file system path where the Corda Node is expecting deployed Cordapp jar files to be placed.
22+
* @param cordaJarPath The absolute file system path where the corda.jar file of the node can be found. This is used to execute database schema migrations where applicable (H2 database in use in development environments).
23+
* @param nodeBaseDirPath The absolute file system path where the base directory of the Corda node can be found. This is used to pass in to corda.jar when being invoked for certain tasks such as executing database schema migrations for a deployed contract.
24+
*/
25+
data class CordappDeploymentConfig(
26+
27+
@get:NotNull
28+
@field:Valid
29+
@field:JsonProperty("sshCredentials") val sshCredentials: CordaNodeSshCredentials,
30+
31+
@get:NotNull
32+
@field:Valid
33+
@field:JsonProperty("rpcCredentials") val rpcCredentials: CordaRpcCredentials,
34+
35+
@get:NotNull
36+
@get:Size(min=1,max=65535)
37+
@field:JsonProperty("cordaNodeStartCmd") val cordaNodeStartCmd: kotlin.String,
38+
39+
@get:NotNull
40+
@get:Size(min=1,max=2048)
41+
@field:JsonProperty("cordappDir") val cordappDir: kotlin.String,
42+
43+
@get:NotNull
44+
@get:Size(min=1,max=2048)
45+
@field:JsonProperty("cordaJarPath") val cordaJarPath: kotlin.String,
46+
47+
@get:NotNull
48+
@get:Size(min=1,max=2048)
49+
@field:JsonProperty("nodeBaseDirPath") val nodeBaseDirPath: kotlin.String
50+
) {
51+
52+
}
53+

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/DeployContractJarsV1Request.kt

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.hyperledger.cactus.plugin.ledger.connector.corda.server.model
22

33
import java.util.Objects
44
import com.fasterxml.jackson.annotation.JsonProperty
5+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.CordappDeploymentConfig
56
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JarFile
67
import javax.validation.constraints.DecimalMax
78
import javax.validation.constraints.DecimalMin
@@ -14,10 +15,16 @@ import javax.validation.Valid
1415

1516
/**
1617
*
18+
* @param cordappDeploymentConfigs The list of deployment configurations pointing to the nodes where the provided cordapp jar files are to be deployed .
1719
* @param jarFiles
1820
*/
1921
data class DeployContractJarsV1Request(
2022

23+
@get:NotNull
24+
@field:Valid
25+
@get:Size(min=1,max=1024)
26+
@field:JsonProperty("cordappDeploymentConfigs") val cordappDeploymentConfigs: kotlin.collections.List<CordappDeploymentConfig>,
27+
2128
@get:NotNull
2229
@field:Valid
2330
@field:JsonProperty("jarFiles") val jarFiles: kotlin.collections.List<JarFile>

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/JarFile.kt

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import javax.validation.Valid
1414
/**
1515
*
1616
* @param filename
17+
* @param hasDbMigrations Indicates whether the cordapp jar in question contains any embedded migrations that Cactus can/should execute between copying the jar into the cordapp directory and starting the node back up.
1718
* @param contentBase64
1819
*/
1920
data class JarFile(
@@ -22,6 +23,9 @@ data class JarFile(
2223
@get:Size(min=1,max=255)
2324
@field:JsonProperty("filename") val filename: kotlin.String,
2425

26+
@get:NotNull
27+
@field:JsonProperty("hasDbMigrations") val hasDbMigrations: kotlin.Boolean,
28+
2529
@get:NotNull
2630
@get:Size(min=1,max=1073741824)
2731
@field:JsonProperty("contentBase64") val contentBase64: kotlin.String

0 commit comments

Comments
 (0)