From 764af04608119345300361dfb27d24ee19e753d3 Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 16 Feb 2018 19:23:47 +0000 Subject: [PATCH 1/3] Moved RPC Proxy to own module in testing/qa/behave/tools Incorporating PR review feedback. Minor fixes to default path and additional copying of startup script. --- .idea/compiler.xml | 2 + settings.gradle | 1 + testing/qa/behave/scripts/update-corda-cts.sh | 39 +++ .../qa/behave/scripts/update-cordapps-cts.sh | 35 +++ .../behave/scripts/update-gradle-plugins.sh | 30 ++ .../qa/behave/scripts/update-r3corda-cts.sh | 45 +++ .../behave/scripts/update-r3cordapps-cts.sh | 40 +++ .../qa/behave/scripts/update-rpcProxy-cts.sh | 25 ++ .../qa/behave/tools/rpc-proxy/build.gradle | 85 +++++ .../service/proxy/CordaRPCProxyClient.kt | 241 +++++++++++++++ .../behave/service/proxy/RPCProxyServer.kt | 93 ++++++ .../service/proxy/RPCProxyWebService.kt | 163 ++++++++++ .../service/proxy/RPCProxyServerTest.kt | 33 ++ .../service/proxy/RPCProxyWebServiceTest.kt | 292 ++++++++++++++++++ .../behave/tools/rpc-proxy/startRPCproxy.sh | 42 +++ 15 files changed, 1166 insertions(+) create mode 100755 testing/qa/behave/scripts/update-corda-cts.sh create mode 100755 testing/qa/behave/scripts/update-cordapps-cts.sh create mode 100755 testing/qa/behave/scripts/update-gradle-plugins.sh create mode 100755 testing/qa/behave/scripts/update-r3corda-cts.sh create mode 100755 testing/qa/behave/scripts/update-r3cordapps-cts.sh create mode 100755 testing/qa/behave/scripts/update-rpcProxy-cts.sh create mode 100644 testing/qa/behave/tools/rpc-proxy/build.gradle create mode 100644 testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt create mode 100644 testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt create mode 100644 testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt create mode 100644 testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt create mode 100644 testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt create mode 100755 testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh diff --git a/.idea/compiler.xml b/.idea/compiler.xml index cf28b922a1..9e5c6e6c17 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -163,6 +163,8 @@ + + diff --git a/settings.gradle b/settings.gradle index 93c5f6b9ca..ffe2af94e0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,6 +43,7 @@ include 'perftestcordapp' ['test-common', 'test-utils', 'smoke-test-utils', 'node-driver'].each { project(":$it").projectDir = new File("$settingsDir/testing/$it") } +include 'testing:qa:behave:tools:rpc-proxy' include 'network-management' include 'network-management:capsule' include 'network-management:capsule-hsm' diff --git a/testing/qa/behave/scripts/update-corda-cts.sh b/testing/qa/behave/scripts/update-corda-cts.sh new file mode 100755 index 0000000000..5189f3ea4e --- /dev/null +++ b/testing/qa/behave/scripts/update-corda-cts.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/corda +# $ testing/qa/behave/scripts/update-corda-cts.sh + +VERSION=master +BUILD_DIR=`pwd` +STAGING_DIR=~/staging/corda/corda-${VERSION} + +# Set up directories +mkdir -p ${STAGING_DIR}/apps + +cd ${BUILD_DIR} + +echo "*************************************************************" +echo "Building and installing $VERSION from $BUILD_DIR" +echo " to $STAGING_DIR" +echo "*************************************************************" + +# Copy Corda capsule into deps +./gradlew clean install +cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${STAGING_DIR}/corda.jar +cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${STAGING_DIR}/apps + +# Build Network Bootstrapper +./gradlew buildBootstrapperJar +cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) ${STAGING_DIR}/network-bootstrapper.jar + +# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC) +./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar +cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar +cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR} \ No newline at end of file diff --git a/testing/qa/behave/scripts/update-cordapps-cts.sh b/testing/qa/behave/scripts/update-cordapps-cts.sh new file mode 100755 index 0000000000..42ac7c5e74 --- /dev/null +++ b/testing/qa/behave/scripts/update-cordapps-cts.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/corda +# $ testing/qa/behave/scripts/update-cordapps-cts.sh + +VERSION=master +BUILD_DIR=`pwd` +SAMPLES_ROOT=`pwd`/../samples +STAGING_DIR=~/staging/corda/corda-${VERSION} + +# Set up directories +mkdir -p ${STAGING_DIR}/apps +mkdir -p ${STAGING_DIR}/proxy + +# Corda repository pinned cordapps +cd ${BUILD_DIR} + +./gradlew samples:simm-valuation-demo:jar +cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/apps + +# Independent cordapps +cd ${SAMPLES_ROOT} + +# Options sample +cd cordapp-option +./gradlew clean jar publishToMavenLocal +cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/apps +cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/proxy diff --git a/testing/qa/behave/scripts/update-gradle-plugins.sh b/testing/qa/behave/scripts/update-gradle-plugins.sh new file mode 100755 index 0000000000..b1dd21203e --- /dev/null +++ b/testing/qa/behave/scripts/update-gradle-plugins.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise +# from v4.x.x => https://github.com/corda/corda-gradle-plugins + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/r3corda +# $ testing/qa/behave/scripts/update-gradle-plugins.sh + +VERSION=master +BUILD_DIR=`pwd` +STAGING_DIR=~/staging/corda/corda-${VERSION} + +# uses gradle plugins directory +cd ${BUILD_DIR} + +# update Gradle plugins (note this is being moved to a new repo https://github.com/corda/corda-gradle-plugins from v4.x.x ) +echo "*************************************************************" +echo "Building and installing `cat constants.properties | grep gradlePluginsVersion`" +echo "*************************************************************" + +cd publish-utils +../gradlew -u install +cd ../ +./gradlew install +cd .. diff --git a/testing/qa/behave/scripts/update-r3corda-cts.sh b/testing/qa/behave/scripts/update-r3corda-cts.sh new file mode 100755 index 0000000000..c15284b0f0 --- /dev/null +++ b/testing/qa/behave/scripts/update-r3corda-cts.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/r3corda +# $ testing/qa/behave/scripts/update-r3corda-cts.sh + +VERSION=master +BUILD_DIR=`pwd` +STAGING_DIR=~/staging/corda/r3corda-${VERSION} + +# Set up directories +mkdir -p ${STAGING_DIR}/apps + +cd ${BUILD_DIR} + +echo "*************************************************************" +echo "Building and installing $VERSION from $BUILD_DIR" +echo "to $STAGING_DIR" +echo "*************************************************************" + +# Copy Corda capsule into deps +./gradlew clean install +cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${STAGING_DIR}/corda.jar + +# Copy Corda libraries into apps +cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${STAGING_DIR}/apps + +# build and distribute Doorman/NMS +./gradlew network-management:capsule:buildDoormanJAR +cp -v $(ls network-management/capsule/build/libs/doorman-*.jar | tail -n1) ${STAGING_DIR}/doorman.jar + +# build and distribute DB Migration tool +./gradlew tools:dbmigration:shadowJar +cp -v $(ls tools/dbmigration/build/libs/*migration-*.jar | tail -n1) ${STAGING_DIR}/dbmigration.jar + +# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC) +./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar +cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar +cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR} \ No newline at end of file diff --git a/testing/qa/behave/scripts/update-r3cordapps-cts.sh b/testing/qa/behave/scripts/update-r3cordapps-cts.sh new file mode 100755 index 0000000000..8f5cd58c6c --- /dev/null +++ b/testing/qa/behave/scripts/update-r3cordapps-cts.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/r3corda +# $ testing/qa/behave/scripts/update-r3cordapps-cts.sh + +VERSION=master +BUILD_DIR=`pwd` +SAMPLES_ROOT=`pwd`/../samples +STAGING_DIR=~/staging/corda/r3corda-${VERSION} + +# Set up directories +mkdir -p ${STAGING_DIR}/apps + +# Corda repository pinned cordapps +cd ${BUILD_DIR} + +./gradlew samples:simm-valuation-demo:jar +cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/apps/simm-valuation-demo.jar +cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/proxy/simm-valuation-demo.jar + +# R3 Corda only +./gradlew tools:notaryhealthcheck:cordaCompileJar +cp -v $(ls tools/notaryhealthcheck/build/libs/notaryhealthcheck-*.jar | tail -n1) ${STAGING_DIR}/apps/notaryhealthcheck.jar +cp -v $(ls tools/notaryhealthcheck/build/libs/notaryhealthcheck-*.jar | tail -n1) ${STAGING_DIR}/proxy/notaryhealthcheck.jar + +# Independent cordapps +cd ${SAMPLES_ROOT} + +# Options sample +cd cordapp-option +./gradlew clean jar publishToMavenLocal +cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/apps +cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/proxy diff --git a/testing/qa/behave/scripts/update-rpcProxy-cts.sh b/testing/qa/behave/scripts/update-rpcProxy-cts.sh new file mode 100755 index 0000000000..77ddce0e8c --- /dev/null +++ b/testing/qa/behave/scripts/update-rpcProxy-cts.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below +# For example: +# corda-master => git clone https://github.com/corda/corda +# r3corda-master => git clone https://github.com/corda/enterprise + +# Please run this script from the corda source code directory +# eg. $ pwd +# /myprojects/r3corda +# $ testing/qa/behave/scripts/update-rpcProxy-cts.sh + +VERSION=master +BUILD_DIR=`pwd` +STAGING_DIR=~/staging/corda/corda-${VERSION} + +# Set up directories +mkdir -p ${STAGING_DIR}/apps + +cd ${BUILD_DIR} + +# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC) +./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar +cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar +cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR} diff --git a/testing/qa/behave/tools/rpc-proxy/build.gradle b/testing/qa/behave/tools/rpc-proxy/build.gradle new file mode 100644 index 0000000000..6e4f9e54bd --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/build.gradle @@ -0,0 +1,85 @@ +/* + * 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. + */ + +group 'net.corda.behave.tools' + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'maven-publish' + +configurations { + smokeTestCompile.extendsFrom testCompile + smokeTestRuntime.extendsFrom testRuntime +} + +sourceSets { + rpcProxy { + kotlin { + srcDir "src/main/kotlin" + } + resources { + srcDir "config/dev" + } + } + smokeTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/smoke-test/kotlin') + } + } +} + +dependencies { + compile project(':test-utils') + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" + + // Integration test helpers + testCompile "org.assertj:assertj-core:$assertj_version" + testCompile "junit:junit:$junit_version" + + compile project(':finance') + compileOnly project(':node-api') + compile project(':core') + compile project(':client:rpc') + + // includes jetty/jersey dependencies used by RPCProxyServer + compile project(':webserver') + + // Jetty http server + rpcProxyCompile "org.eclipse.jetty:jetty-servlet:$jetty_version" + rpcProxyCompile "org.eclipse.jetty:jetty-webapp:$jetty_version" + rpcProxyCompile "javax.servlet:javax.servlet-api:3.1.0" + + // Jersey for JAX-RS implementation for use in Jetty + rpcProxyCompile "org.glassfish.jersey.core:jersey-server:$jersey_version" + rpcProxyCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:$jersey_version" + rpcProxyCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:$jersey_version" + +} + +task smokeTest(type: Test) { + testClassesDirs = sourceSets.smokeTest.output.classesDirs + classpath = sourceSets.smokeTest.runtimeClasspath +} + +task rpcProxyJar(type: Jar) { + baseName "corda-rpcProxy" + from { + configurations.rpcProxyRuntime.collect { it.isDirectory() ? it : zipTree(it) } + } + with jar + exclude("META-INF/*.DSA") + exclude("META-INF/*.RSA") + exclude("META-INF/*.SF") + manifest { + attributes 'Main-Class': 'net.corda.behave.service.proxy.RPCProxyServerKt' + } +} diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt new file mode 100644 index 0000000000..7d37fadfb7 --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt @@ -0,0 +1,241 @@ +package net.corda.behave.service.proxy + +import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.ContractState +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.doneFuture +import net.corda.core.internal.openHttpConnection +import net.corda.core.internal.responseAs +import net.corda.core.messaging.* +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.* +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import java.io.InputStream +import java.security.PublicKey +import java.time.Instant + +class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : CordaRPCOps { + + companion object { + val log = contextLogger() + } + + init { + try { + KryoClientSerializationScheme.initialiseSerialization() + } catch (e: Exception) { log.warn("Kryo RPC Client serialization already initialised.")} + } + + override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle { + val flowName = logicType.name + val argList = listOf(flowName, *args) + + log.info("Corda RPC Proxy client calling: $flowName with values: $argList") + val response = doPost(targetHostAndPort, "start-flow", argList.serialize().bytes) + val result = doneFuture(response) + return FlowHandleImpl(StateMachineRunId.createRandom(), result) as FlowHandle + } + + override fun nodeInfo(): NodeInfo { + return doGet(targetHostAndPort, "node-info") + } + + override fun notaryIdentities(): List { + return doGet(targetHostAndPort, "notary-identities") + } + + override fun vaultQuery(contractStateType: Class): Vault.Page { + return doPost(targetHostAndPort, "vault-query", contractStateType.name.serialize().bytes) + } + + override fun networkMapSnapshot(): List { + return doGet(targetHostAndPort, "network-map-snapshot") + } + + override fun stateMachinesSnapshot(): List { + TODO("not implemented") + } + + override fun stateMachinesFeed(): DataFeed, StateMachineUpdate> { + TODO("not implemented") + } + + override fun vaultQueryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { + TODO("not implemented") + } + + override fun vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class): Vault.Page { + TODO("not implemented") + } + + override fun vaultQueryByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + TODO("not implemented") + } + + override fun vaultQueryByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { + TODO("not implemented") + } + + override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { + TODO("not implemented") + } + + override fun vaultTrack(contractStateType: Class): DataFeed, Vault.Update> { + TODO("not implemented") + } + + override fun vaultTrackByCriteria(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { + TODO("not implemented") + } + + override fun vaultTrackByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { + TODO("not implemented") + } + + override fun vaultTrackByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { + TODO("not implemented") + } + + override fun internalVerifiedTransactionsSnapshot(): List { + TODO("not implemented") + } + + override fun internalVerifiedTransactionsFeed(): DataFeed, SignedTransaction> { + TODO("not implemented") + } + + override fun stateMachineRecordedTransactionMappingSnapshot(): List { + TODO("not implemented") + } + + override fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> { + TODO("not implemented") + } + + override fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> { + TODO("not implemented") + } + + override fun networkParametersFeed(): DataFeed { + TODO("not implemented") + } + + override fun acceptNewNetworkParameters(parametersHash: SecureHash) { + TODO("not implemented") + } + + override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle { + TODO("not implemented") + } + + override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) { + TODO("not implemented") + } + + override fun getVaultTransactionNotes(txnId: SecureHash): Iterable { + TODO("not implemented") + } + + override fun attachmentExists(id: SecureHash): Boolean { + TODO("not implemented") + } + + override fun openAttachment(id: SecureHash): InputStream { + TODO("not implemented") + } + + override fun uploadAttachment(jar: InputStream): SecureHash { + TODO("not implemented") + } + + override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash { + TODO("not implemented") + } + + override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List { + TODO("not implemented") + } + + override fun currentNodeTime(): Instant { + TODO("not implemented") + } + + override fun waitUntilNetworkReady(): CordaFuture { + TODO("not implemented") + } + + override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? { + TODO("not implemented") + } + + override fun partyFromKey(key: PublicKey): Party? { + TODO("not implemented") + } + + override fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party? { + TODO("not implemented") + } + + override fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? { + TODO("not implemented") + } + + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + return doPost(targetHostAndPort, "parties-from-name", query.serialize().bytes) + } + + override fun registeredFlows(): List { + return doGet(targetHostAndPort, "registered-flows") + } + + override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? { + TODO("not implemented") + } + + override fun clearNetworkMapCache() { + TODO("not implemented") + } + + override fun setFlowsDrainingModeEnabled(enabled: Boolean) { + TODO("not implemented") + } + + override fun isFlowsDrainingModeEnabled(): Boolean { + TODO("not implemented") + } + + override fun shutdown() { + TODO("not implemented") + } + + override fun killFlow(id: StateMachineRunId): Boolean { + TODO("not implemented") + } + + private inline fun doPost(hostAndPort: NetworkHostAndPort, path: String, payload: ByteArray) : T { + val url = java.net.URL("http://$hostAndPort/rpc/$path") + val connection = url.openHttpConnection().apply { + doOutput = true + requestMethod = "POST" + setRequestProperty("Content-Type", javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM) + outputStream.write(payload) + } + return connection.responseAs() + } + + private inline fun doGet(hostAndPort: NetworkHostAndPort, path: String): T { + return java.net.URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() + } +} \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt new file mode 100644 index 0000000000..b2545c32c6 --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt @@ -0,0 +1,93 @@ +package net.corda.behave.service.proxy + +import net.corda.behave.service.proxy.RPCProxyServer.Companion.initialiseSerialization +import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal.nodeSerializationEnv +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.handler.HandlerCollection +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import java.io.Closeable +import java.net.InetSocketAddress + +class RPCProxyServer(hostAndPort: NetworkHostAndPort, val webService: RPCProxyWebService) : Closeable { + + fun start(): Boolean { + log.info("Starting RPC Proxy web services...") + try { + buildServletContextHandler() + server.start() + } + catch(e: Exception) { + log.info("Failed to start RPC Proxy server: ${e.message}") + return false + } + log.info("RPC Proxy web services started on $hostAndPort with ${webService.javaClass.simpleName}}") + + return true + } + + override fun close() { + log.info("Shutting down RPC Proxy web services...") + server.stop() + server.join() + } + + private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { + handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } + } + + private val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } + .first() + + private fun buildServletContextHandler(): ServletContextHandler { + return ServletContextHandler().apply { + contextPath = "/" + val resourceConfig = ResourceConfig().apply { + // Add your API provider classes (annotated for JAX-RS) here + register(webService) + } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start + addServlet(jerseyServlet, "/*") + } + } + + companion object { + val log = contextLogger() + fun initialiseSerialization() { + try { + nodeSerializationEnv = + SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(KryoClientSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) + }, + AMQP_P2P_CONTEXT, + rpcClientContext = KRYO_RPC_CLIENT_CONTEXT) + } + catch(e: Exception) { log.warn("Skipping initialiseSerialization: ${e.message}") } + } + } +} + +fun main(args: Array) { + initialiseSerialization() + val portNo = args.singleOrNull() ?: throw IllegalArgumentException("Please specify a port number") + val hostAndPort = NetworkHostAndPort("localhost", portNo.toIntOrNull() ?: 13000) + println("Starting RPC Proxy Server on [$hostAndPort] ...") + RPCProxyServer(hostAndPort, webService = RPCProxyWebService(hostAndPort)).start() +} \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt new file mode 100644 index 0000000000..10ba274a76 --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt @@ -0,0 +1,163 @@ +package net.corda.behave.service.proxy + +import net.corda.behave.service.proxy.RPCProxyWebService.Companion.RPC_PROXY_PATH +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.core.contracts.ContractState +import net.corda.core.flows.FlowLogic +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import java.io.InputStream +import javax.servlet.http.HttpServletRequest +import javax.ws.rs.Consumes +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.status + +@Path(RPC_PROXY_PATH) +class RPCProxyWebService(targetHostAndPort: NetworkHostAndPort) { + + // see "NetworkInterface" port allocation definitions + private val targetPort = targetHostAndPort.port - 1000 + + init { + try { + effectiveSerializationEnv + } catch (e: IllegalStateException) { + try { + KryoClientSerializationScheme.initialiseSerialization() + } catch (e: IllegalStateException) { + // Race e.g. two of these constructed in parallel, ignore. + } + } + } + + companion object { + private val log = contextLogger() + const val DEFAULT_PASSWORD = "S0meS3cretW0rd" + const val RPC_PROXY_PATH = "rpc" + } + + @GET + @Path("my-ip") + fun myIp(@Context request: HttpServletRequest): Response { + return createResponse("HELLO! My ip is ${request.remoteHost}:${request.remotePort}") + } + + @GET + @Path("node-info") + fun nodeInfo(@Context request: HttpServletRequest): Response { + log.info("nodeInfo") + return use { + it.nodeInfo() + } + } + + @GET + @Path("registered-flows") + fun registeredFlows(@Context request: HttpServletRequest): Response { + log.info("registeredFlows") + return use { + it.registeredFlows() + } + } + + @GET + @Path("notary-identities") + fun notaryIdentities(@Context request: HttpServletRequest): Response { + log.info("networkMapSnapshot") + return use { + it.notaryIdentities() + } + } + + @GET + @Path("network-map-snapshot") + fun networkMapSnapshot(@Context request: HttpServletRequest): Response { + log.info("networkMapSnapshot") + return use { + it.networkMapSnapshot() + } + } + + @POST + @Path("parties-from-name") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + fun partiesFromName(input: InputStream): Response { + log.info("partiesFromName") + val queryName = input.readBytes().deserialize() + return use { + it.partiesFromName(queryName, false) + } + } + + @POST + @Path("vault-query") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + fun vaultQuery(input: InputStream): Response { + log.info("vaultQuery") + val contractStateType = input.readBytes().deserialize() + val clazz = Class.forName(contractStateType) as Class + return use { + log.info("Calling vaultQuery with: $clazz") + it.vaultQuery(clazz) + } + } + + @POST + @Path("start-flow") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + fun startFlow(input: InputStream): Response { + log.info("startFlow") + return use { rpcClient -> + val argsList = input.readBytes().deserialize>() + for (i in argsList.indices) { + log.info("$i: ${argsList[i]}") + } + val flowClass = Class.forName(argsList[0] as String) as Class> + val flowArgs = argsList.drop(1).toTypedArray() + log.info("Calling flow: $flowClass with arguments: ${flowArgs.asList()}") + rpcClient.startFlowDynamic(flowClass, *flowArgs).returnValue.getOrThrow() + } + } + + private fun use(action: (CordaRPCOps) -> T): Response { + val targetHost = NetworkHostAndPort("localhost", targetPort) + val config = object : CordaRPCClientConfiguration { + override val connectionMaxRetryInterval = 10.seconds + } + log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...") + return try { + CordaRPCClient(targetHost, config).use("corda", DEFAULT_PASSWORD) { + log.info("RPC connection to ${targetHost.host}:${targetHost.port} established") + val client = it.proxy + val result = action(client) + log.info("CordaRPCOps result: $result") + return createResponse(result) + } + } catch (e: Exception) { + log.warn("RPC Proxy request failed: ", e) + e.printStackTrace() + status(Response.Status.INTERNAL_SERVER_ERROR).encoding(e.message) + }.build() + } + + private fun createResponse(payload: Any?): Response { + return if (payload != null) { + Response.ok(payload.serialize().bytes) + } else { + status(Response.Status.NOT_FOUND) + }.build() + } +} diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt new file mode 100644 index 0000000000..ae2e321bb6 --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt @@ -0,0 +1,33 @@ +package net.corda.behave.service.proxy + +import net.corda.core.internal.checkOkResponse +import net.corda.core.internal.openHttpConnection +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import org.junit.Test + +class RPCProxyServerTest { + + private val rpcProxyHostAndPort = NetworkHostAndPort("localhost", 13000) + private val nodeHostAndPort = NetworkHostAndPort("localhost", 12000) + + @Test + fun `execute RPCOp`() { + RPCProxyServer(rpcProxyHostAndPort, + webService = RPCProxyWebService(nodeHostAndPort)).use { + it.start() + it.doPost("rpcOps", OpaqueBytes.of(0).bytes) + } + } + + private fun RPCProxyServer.doPost(path: String, payload: ByteArray) { + val url = java.net.URL("http://$rpcProxyHostAndPort/rpc/$path") + url.openHttpConnection().apply { + doOutput = true + requestMethod = "POST" + setRequestProperty("Content-Type", javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM) + outputStream.write(payload) + checkOkResponse() + } + } +} \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt new file mode 100644 index 0000000000..829b31999b --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt @@ -0,0 +1,292 @@ +package net.corda.behave.service.proxy + +import net.corda.core.internal.openHttpConnection +import net.corda.core.internal.responseAs +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.finance.SWISS_FRANCS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashExitFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class RPCProxyWebServiceTest { + + /** + * client -> HTTPtoRPCProxy -> Corda Node + */ + private val hostAndPort = NetworkHostAndPort("localhost", 13002) + private val rpcProxyClient = CordaRPCProxyClient(hostAndPort) + + private val hostAndPortB = NetworkHostAndPort("localhost", 13007) + private val rpcProxyClientB = CordaRPCProxyClient(hostAndPortB) + + private val hostAndPortC = NetworkHostAndPort("localhost", 13012) + private val rpcProxyClientC = CordaRPCProxyClient(hostAndPortC) + + @Test + fun myIp() { + val response = doGet("my-ip") + println(response) + assertTrue(response.contains("My ip is")) + } + + @Test + fun nodeInfo() { + val response = rpcProxyClient.nodeInfo() + println(response) + } + + @Test + fun registeredFlows() { + val response = rpcProxyClient.registeredFlows() + println(response) + } + + @Test + fun notaryIdentities() { + val response = rpcProxyClient.notaryIdentities() + println(response) + } + + @Test + fun networkMapSnapshot() { + val response = rpcProxyClient.networkMapSnapshot() + println(response) + } + + @Test + fun startFlowCashIssuePartyA() { + val notary = rpcProxyClient.notaryIdentities()[0] + val response = rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startFlowCashIssuePartyB() { + val notary = rpcProxyClientB.notaryIdentities()[0] + val response = rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(1000), OpaqueBytes.of(1), notary) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startFlowCashPaymentToPartyC() { + val recipient = rpcProxyClientB.partiesFromName("PartyC", false).first() + val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(100), recipient) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startFlowCashPaymentToPartyB() { + val recipient = rpcProxyClient.partiesFromName("PartyB", false).first() + val response = rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), recipient) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startFlowCashPaymentToPartyA() { + val recipient = rpcProxyClientB.partiesFromName("PartyA", false).first() + val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(500), recipient) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startFlowCashExit() { + val response = rpcProxyClient.startFlow(::CashExitFlow, POUNDS(500), OpaqueBytes.of(1)) + val result = response.returnValue.getOrThrow().stx + println(result) + } + + @Test + fun startMultiPartyCashFlows() { + val notary = rpcProxyClient.notaryIdentities()[0] + + // Party A issue 500 GBP + println("Party A issuing 500 GBP") + rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party B issue 1000 USD + println("Party B issuing 1000 USD") + rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(1000), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party A transfers 250 GBP to Party B + println("Party A transferring 250 GBP to Party B") + val partyB = rpcProxyClient.partiesFromName("PartyB", false).first() + rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB).returnValue.getOrThrow() + + // Party B transfers 500 USD to Party A + println("Party B transferring 500 USD to Party A") + val partyA = rpcProxyClientB.partiesFromName("PartyA", false).first() + rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(500), partyA).returnValue.getOrThrow() + + // Party B transfers back 125 GBP to Party A + println("Party B transferring back 125 GBP to Party A") + rpcProxyClientB.startFlow(::CashPaymentFlow, POUNDS(125), partyA).returnValue.getOrThrow() + + // Party A transfers back 250 USD to Party B + println("Party A transferring back 250 USD to Party B") + rpcProxyClient.startFlow(::CashPaymentFlow, DOLLARS(250), partyB).returnValue.getOrThrow() + + // Query the Vault of each respective Party + val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java) + responseA.states.forEach { state -> + println("PartyA: ${state.state.data.amount}") + } + + val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java) + responseB.states.forEach { state -> + println("PartyB: ${state.state.data.amount}") + } + } + + @Test + fun startMultiABCPartyCashFlows() { + val notary = rpcProxyClient.notaryIdentities()[0] + + while(true) { + // Party A issue 500 GBP + println("Party A issuing 500 GBP") + rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party B issue 500 USD + println("Party B issuing 500 USD") + rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party C issue 500 CHF + println("Party C issuing 500 CHF") + rpcProxyClientC.startFlow(::CashIssueFlow, SWISS_FRANCS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party A transfers 250 GBP to Party B who transfers to party C + println("Party A transferring 250 GBP to Party B") + val partyB = rpcProxyClient.partiesFromName("PartyB", false).first() + rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB).returnValue.getOrThrow() + + println(" ... and forwarding to Party C") + val partyC = rpcProxyClientB.partiesFromName("PartyC", false).first() + rpcProxyClientB.startFlow(::CashPaymentFlow, POUNDS(250), partyC).returnValue.getOrThrow() + + // Party B transfers 500 USD to Party C who transfers to party A + println("Party B transferring 500 USD to Party C") + rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(250), partyC).returnValue.getOrThrow() + + println(" ... and forwarding to Party A") + val partyA = rpcProxyClientC.partiesFromName("PartyA", false).first() + rpcProxyClientC.startFlow(::CashPaymentFlow, DOLLARS(250), partyA).returnValue.getOrThrow() + + // Party C transfers 550 CHF to Party A who transfers to party B + println("Party C transferring 250 CHF to Party A") + rpcProxyClientC.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyA).returnValue.getOrThrow() + + println(" ... and forwarding to Party B") + rpcProxyClient.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyB).returnValue.getOrThrow() + + // Query the Vault of each respective Party + val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java) + responseA.states.forEach { state -> + println("PartyA: ${state.state.data.amount}") + } + + val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java) + responseB.states.forEach { state -> + println("PartyB: ${state.state.data.amount}") + } + + val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java) + responseC.states.forEach { state -> + println("PartyC: ${state.state.data.amount}") + } + + println("============================================================================================") + } + } + + // enable Flow Draining on Node B + @Test + fun startMultiACPartyCashFlows() { + val notary = rpcProxyClient.notaryIdentities()[0] + + while(true) { + // Party A issue 500 GBP + println("Party A issuing 500 GBP") + rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party C issue 500 CHF + println("Party C issuing 500 CHF") + rpcProxyClientC.startFlow(::CashIssueFlow, SWISS_FRANCS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow() + + // Party A transfers 250 GBP to Party B who transfers to party C + println("Party A transferring 250 GBP to Party B") + val partyB = rpcProxyClient.partiesFromName("PartyB", false).first() + rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB) // BLOCKS!!!!! + + println(" ... and forwarding to Party A") + val partyA = rpcProxyClientC.partiesFromName("PartyA", false).first() + rpcProxyClientC.startFlow(::CashPaymentFlow, DOLLARS(250), partyA).returnValue.getOrThrow() + + // Party C transfers 550 CHF to Party A who transfers to party B + println("Party C transferring 250 CHF to Party A") + rpcProxyClientC.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyA).returnValue.getOrThrow() + + println(" ... and forwarding to Party B") + rpcProxyClient.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyB) + + // Query the Vault of each respective Party + val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java) + responseA.states.forEach { state -> + println("PartyA: ${state.state.data.amount}") + } + + val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java) + responseB.states.forEach { state -> + println("PartyB: ${state.state.data.amount}") + } + + val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java) + responseC.states.forEach { state -> + println("PartyC: ${state.state.data.amount}") + } + + println("============================================================================================") + } + } + + @Test + fun vaultQueryCash() { + try { + val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java) + responseA.states.forEach { state -> + println("PartyA: ${state.state.data.amount}") + } + + val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java) + responseB.states.forEach { state -> + println("PartyB: ${state.state.data.amount}") + } + + val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java) + responseC.states.forEach { state -> + println("PartyC: ${state.state.data.amount}") + } + } + catch (e: Exception) { + println("Vault Cash query error: ${e.message}") + fail() + } + } + + private inline fun doGet(path: String): T { + return java.net.URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() + } +} \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh new file mode 100755 index 0000000000..d6d19e89e1 --- /dev/null +++ b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +DISTRO_DIR=$1 +PORT="${2:-13000}" + +if [ ! -d "$DISTRO_DIR" ]; then + echo "Must specify location of Corda distribution (directory does not exist: $DISTRO_DIR)" + exit 1 +fi + +if [ ! -f "$DISTRO_DIR/corda.jar" ]; then + echo "Distribution corda.jar not found" + exit 1 +fi + +if [ ! -f "$DISTRO_DIR/corda-rpcProxy.jar" ]; then + echo "Distribution corda-rpcProxy.jar not found" + exit 1 +fi + +# unzip corda jars into proxy directory (if not already there) +if [ ! -d "$DISTRO_DIR/proxy" ]; then + mkdir -p ${DISTRO_DIR}/proxy + unzip ${DISTRO_DIR}/corda.jar -d ${DISTRO_DIR}/proxy +fi + +# launch proxy +echo "Launching RPC proxy ..." +echo "java -cp $DISTRO_DIR/corda-rpcProxy.jar:\ +\n\t$(ls ${DISTRO_DIR}/proxy/*.jar | tr '\n' ':'):\ +\n\t$(ls ${DISTRO_DIR}/apps/*.jar | tr '\n' ':') +\ +\n\tnet.corda.behave.service.proxy.RPCProxyServerKt ${PORT} +" + +/usr/bin/java -cp ${DISTRO_DIR}/corda-rpcProxy.jar:\ +$(ls ${DISTRO_DIR}/proxy/*.jar | tr '\n' ':'):\ +$(ls ${DISTRO_DIR}/apps/*.jar | tr '\n' ':') \ +net.corda.behave.service.proxy.RPCProxyServerKt ${PORT} &> rpcproxy-${PORT}.log & + +echo $! > /tmp/rpcProxy-pid-${PORT} +echo "RPCProxyServer PID: $(cat /tmp/rpcProxy-pid-${PORT})" From ccd5f5fd5bcb96fb2982f92d6bd4f50f58eb000f Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 2 May 2018 10:47:12 +0100 Subject: [PATCH 2/3] Re-apply previous fixes. --- .../behave/service/proxy/RPCProxyServer.kt | 3 +- .../service/proxy/RPCProxyServerTest.kt | 14 ++------ .../service/proxy/RPCProxyWebServiceTest.kt | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt index b2545c32c6..df76acbaae 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt @@ -1,6 +1,7 @@ package net.corda.behave.service.proxy import net.corda.behave.service.proxy.RPCProxyServer.Companion.initialiseSerialization +import net.corda.behave.service.proxy.RPCProxyServer.Companion.log import net.corda.client.rpc.internal.KryoClientSerializationScheme import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv @@ -88,6 +89,6 @@ fun main(args: Array) { initialiseSerialization() val portNo = args.singleOrNull() ?: throw IllegalArgumentException("Please specify a port number") val hostAndPort = NetworkHostAndPort("localhost", portNo.toIntOrNull() ?: 13000) - println("Starting RPC Proxy Server on [$hostAndPort] ...") + log.info("Starting RPC Proxy Server on [$hostAndPort] ...") RPCProxyServer(hostAndPort, webService = RPCProxyWebService(hostAndPort)).start() } \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt index ae2e321bb6..f8daf14425 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt @@ -3,7 +3,6 @@ package net.corda.behave.service.proxy import net.corda.core.internal.checkOkResponse import net.corda.core.internal.openHttpConnection import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.OpaqueBytes import org.junit.Test class RPCProxyServerTest { @@ -16,18 +15,11 @@ class RPCProxyServerTest { RPCProxyServer(rpcProxyHostAndPort, webService = RPCProxyWebService(nodeHostAndPort)).use { it.start() - it.doPost("rpcOps", OpaqueBytes.of(0).bytes) + it.doGet("my-ip") } } - private fun RPCProxyServer.doPost(path: String, payload: ByteArray) { - val url = java.net.URL("http://$rpcProxyHostAndPort/rpc/$path") - url.openHttpConnection().apply { - doOutput = true - requestMethod = "POST" - setRequestProperty("Content-Type", javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM) - outputStream.write(payload) - checkOkResponse() - } + private fun RPCProxyServer.doGet(path: String) { + return java.net.URL("http://$rpcProxyHostAndPort/rpc/$path").openHttpConnection().checkOkResponse() } } \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt index 829b31999b..f29646ff45 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt @@ -2,7 +2,9 @@ package net.corda.behave.service.proxy import net.corda.core.internal.openHttpConnection import net.corda.core.internal.responseAs +import net.corda.core.internal.sumByLong import net.corda.core.messaging.startFlow +import net.corda.core.node.services.Vault import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -13,14 +15,19 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertTrue import org.junit.Assert.fail +import org.junit.Ignore import org.junit.Test class RPCProxyWebServiceTest { /** * client -> HTTPtoRPCProxy -> Corda Node + * + * Please note these tests require a running network with at 3 Nodes + * (listening on ports 12002, 12007, and 12012) and a Notary */ private val hostAndPort = NetworkHostAndPort("localhost", 13002) private val rpcProxyClient = CordaRPCProxyClient(hostAndPort) @@ -42,24 +49,32 @@ class RPCProxyWebServiceTest { fun nodeInfo() { val response = rpcProxyClient.nodeInfo() println(response) + assertThat(response.toString()).matches("NodeInfo\\(addresses=\\[.*\\], legalIdentitiesAndCerts=\\[.*\\], platformVersion=.*, serial=.*\\)") } @Test fun registeredFlows() { val response = rpcProxyClient.registeredFlows() println(response) + // Node built-in flows + assertThat(response).contains("net.corda.core.flows.ContractUpgradeFlow\$Authorise", + "net.corda.core.flows.ContractUpgradeFlow\$Deauthorise", + "net.corda.core.flows.ContractUpgradeFlow\$Initiate") } @Test fun notaryIdentities() { val response = rpcProxyClient.notaryIdentities() println(response) + assertThat(response.first().name.toString()).isEqualTo("O=Notary, L=London, C=GB") } @Test fun networkMapSnapshot() { val response = rpcProxyClient.networkMapSnapshot() println(response) + assertThat(response).contains(rpcProxyClient.nodeInfo()) + assertThat(response.size).isEqualTo(4) } @Test @@ -68,6 +83,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -76,6 +92,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(1000), OpaqueBytes.of(1), notary) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -84,6 +101,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(100), recipient) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -92,6 +110,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), recipient) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -100,6 +119,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(500), recipient) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -107,6 +127,7 @@ class RPCProxyWebServiceTest { val response = rpcProxyClient.startFlow(::CashExitFlow, POUNDS(500), OpaqueBytes.of(1)) val result = response.returnValue.getOrThrow().stx println(result) + assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)") } @Test @@ -149,8 +170,11 @@ class RPCProxyWebServiceTest { responseB.states.forEach { state -> println("PartyB: ${state.state.data.amount}") } + + assertVaultHoldsCash(responseA, responseB) } + @Ignore @Test fun startMultiABCPartyCashFlows() { val notary = rpcProxyClient.notaryIdentities()[0] @@ -213,6 +237,7 @@ class RPCProxyWebServiceTest { } // enable Flow Draining on Node B + @Ignore @Test fun startMultiACPartyCashFlows() { val notary = rpcProxyClient.notaryIdentities()[0] @@ -279,6 +304,8 @@ class RPCProxyWebServiceTest { responseC.states.forEach { state -> println("PartyC: ${state.state.data.amount}") } + + assertVaultHoldsCash(responseA, responseB, responseC) } catch (e: Exception) { println("Vault Cash query error: ${e.message}") @@ -286,6 +313,15 @@ class RPCProxyWebServiceTest { } } + private fun assertVaultHoldsCash(vararg vaultPages: Vault.Page) { + vaultPages.forEach { vaultPage -> + assertThat(vaultPage.states.size).isGreaterThan(0) + vaultPage.states.groupBy { it.state.data.amount.token.product.currencyCode }.forEach { _, value -> + assertThat(value.sumByLong { it.state.data.amount.quantity }).isGreaterThan(0L) + } + } + } + private inline fun doGet(path: String): T { return java.net.URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() } From f013c1dcb776e04f243cb4cfebb471c10310a5a8 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 2 May 2018 11:09:25 +0100 Subject: [PATCH 3/3] Address remaining PR review comments. --- .../service/proxy/CordaRPCProxyClient.kt | 24 ++++++++++--------- .../behave/service/proxy/RPCProxyServer.kt | 2 +- .../service/proxy/RPCProxyServerTest.kt | 3 ++- .../service/proxy/RPCProxyWebServiceTest.kt | 3 ++- .../behave/tools/rpc-proxy/startRPCproxy.sh | 5 ++-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt index 7d37fadfb7..01456ca21d 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt @@ -23,8 +23,10 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import java.io.InputStream +import java.net.URL import java.security.PublicKey import java.time.Instant +import javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : CordaRPCOps { @@ -64,6 +66,14 @@ class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : C return doGet(targetHostAndPort, "network-map-snapshot") } + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + return doPost(targetHostAndPort, "parties-from-name", query.serialize().bytes) + } + + override fun registeredFlows(): List { + return doGet(targetHostAndPort, "registered-flows") + } + override fun stateMachinesSnapshot(): List { TODO("not implemented") } @@ -192,14 +202,6 @@ class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : C TODO("not implemented") } - override fun partiesFromName(query: String, exactMatch: Boolean): Set { - return doPost(targetHostAndPort, "parties-from-name", query.serialize().bytes) - } - - override fun registeredFlows(): List { - return doGet(targetHostAndPort, "registered-flows") - } - override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? { TODO("not implemented") } @@ -225,17 +227,17 @@ class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : C } private inline fun doPost(hostAndPort: NetworkHostAndPort, path: String, payload: ByteArray) : T { - val url = java.net.URL("http://$hostAndPort/rpc/$path") + val url = URL("http://$hostAndPort/rpc/$path") val connection = url.openHttpConnection().apply { doOutput = true requestMethod = "POST" - setRequestProperty("Content-Type", javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM) + setRequestProperty("Content-Type", APPLICATION_OCTET_STREAM) outputStream.write(payload) } return connection.responseAs() } private inline fun doGet(hostAndPort: NetworkHostAndPort, path: String): T { - return java.net.URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() + return URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() } } \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt index df76acbaae..e5e4370635 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt @@ -30,7 +30,7 @@ class RPCProxyServer(hostAndPort: NetworkHostAndPort, val webService: RPCProxyWe server.start() } catch(e: Exception) { - log.info("Failed to start RPC Proxy server: ${e.message}") + log.error("Failed to start RPC Proxy server: ${e.message}", e) return false } log.info("RPC Proxy web services started on $hostAndPort with ${webService.javaClass.simpleName}}") diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt index f8daf14425..7d93eaa863 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt @@ -4,6 +4,7 @@ import net.corda.core.internal.checkOkResponse import net.corda.core.internal.openHttpConnection import net.corda.core.utilities.NetworkHostAndPort import org.junit.Test +import java.net.URL class RPCProxyServerTest { @@ -20,6 +21,6 @@ class RPCProxyServerTest { } private fun RPCProxyServer.doGet(path: String) { - return java.net.URL("http://$rpcProxyHostAndPort/rpc/$path").openHttpConnection().checkOkResponse() + return URL("http://$rpcProxyHostAndPort/rpc/$path").openHttpConnection().checkOkResponse() } } \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt index f29646ff45..73063737ff 100644 --- a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt +++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt @@ -20,6 +20,7 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Ignore import org.junit.Test +import java.net.URL class RPCProxyWebServiceTest { @@ -323,6 +324,6 @@ class RPCProxyWebServiceTest { } private inline fun doGet(path: String): T { - return java.net.URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() + return URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs() } } \ No newline at end of file diff --git a/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh index d6d19e89e1..6cf6aa93be 100755 --- a/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh +++ b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh @@ -2,6 +2,7 @@ DISTRO_DIR=$1 PORT="${2:-13000}" +TMPDIR="${TMPDIR:-/tmp}" if [ ! -d "$DISTRO_DIR" ]; then echo "Must specify location of Corda distribution (directory does not exist: $DISTRO_DIR)" @@ -38,5 +39,5 @@ $(ls ${DISTRO_DIR}/proxy/*.jar | tr '\n' ':'):\ $(ls ${DISTRO_DIR}/apps/*.jar | tr '\n' ':') \ net.corda.behave.service.proxy.RPCProxyServerKt ${PORT} &> rpcproxy-${PORT}.log & -echo $! > /tmp/rpcProxy-pid-${PORT} -echo "RPCProxyServer PID: $(cat /tmp/rpcProxy-pid-${PORT})" +echo $! > ${TMPDIR}/rpcProxy-pid-${PORT} +echo "RPCProxyServer PID: $(cat ${TMPDIR}/rpcProxy-pid-${PORT})"