diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index ccc50081dd..acec9f280d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -31,13 +31,13 @@ import net.corda.core.CordaRuntimeException * the exception is handled. This ID is propagated to counterparty flows, even when the [FlowException] is * downgraded to an [UnexpectedFlowEndException]. This is so the error conditions may be correlated later on. */ -open class FlowException(message: String?, cause: Throwable?) : +open class FlowException(message: String?, cause: Throwable?, var originalErrorId: Long? = null) : CordaException(message, cause), IdentifiableException { + constructor(message: String?, cause: Throwable?) : this(message, cause, null) constructor(message: String?) : this(message, null) constructor(cause: Throwable?) : this(cause?.toString(), cause) constructor() : this(null, null) - var originalErrorId: Long? = null override fun getErrorId(): Long? = originalErrorId } // DOCEND 1 diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index aff26f2943..409d199ada 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -30,6 +30,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.core.ALICE_NAME @@ -147,6 +148,7 @@ class ContractUpgradeFlowTest { startFlow(), startFlow() )) + val expectedExceptionClass = if (aliceNode.internals.configuration.devMode) CordaRuntimeException::class else InternalNodeException::class val rpcA = startProxy(aliceNode, user) val rpcB = startProxy(bobNode, user) val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bob)) @@ -163,7 +165,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - assertFailsWith(CordaRuntimeException::class) { rejectedFuture.getOrThrow() } + assertFailsWith(expectedExceptionClass) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade, and immediately deauthorise the same. rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, @@ -178,7 +180,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - assertFailsWith(CordaRuntimeException::class) { deauthorisedFuture.getOrThrow() } + assertFailsWith(expectedExceptionClass) { deauthorisedFuture.getOrThrow() } // Party B authorise the contract state upgrade. rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a8dbf38efa..290aac5560 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -10,6 +10,8 @@ Unreleased * ``WireTransaction.Companion.createComponentGroups`` has been marked as ``@CordaInternal``. It was never intended to be public and was already internal for Kotlin code. +* RPC server will now mask internal errors to RPC clients if not in devMode. ``Throwable``s implementing ``ClientRelevantError`` will continue to be propagated to clients. + * RPC Framework moved from Kryo to the Corda AMQP implementation [Corda-847]. This completes the removal of ``Kryo`` from general use within Corda, remaining only for use in flow checkpointing. diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 6bf0f91483..097ccf4c31 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -298,9 +298,14 @@ Error handling -------------- If something goes wrong with the RPC infrastructure itself, an ``RPCException`` is thrown. If you call a method that requires a higher version of the protocol than the server supports, ``UnsupportedOperationException`` is thrown. -Otherwise, if the server implementation throws an exception, that exception is serialised and rethrown on the client +Otherwise the behaviour depends on the ``devMode`` node configuration option. + +In ``devMode``, if the server implementation throws an exception, that exception is serialised and rethrown on the client side as if it was thrown from inside the called RPC method. These exceptions can be caught as normal. +When not in ``devMode``, the server will mask exceptions not meant for clients and return an ``InternalNodeException`` instead. +This does not expose internal information to clients, strengthening privacy and security. CorDapps can have exceptions implement ``ClientRelevantError`` to allow them to reach RPC clients. + Connection management --------------------- It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()`` diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index d1a89b987a..28ddd0162f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -13,6 +13,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.CordaRuntimeException +import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.internal.div @@ -22,22 +23,29 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.internal.NodeStartup import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.User +import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.ClassRule import org.junit.Test -import java.io.* +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable import kotlin.test.assertEquals class BootTests : IntegrationTest() { @@ -50,12 +58,21 @@ class BootTests : IntegrationTest() { @Test fun `java deserialization is disabled`() { + val user = User("u", "p", setOf(startFlow())) + val params = NodeParameters(rpcUsers = listOf(user)) + + fun NodeHandle.attemptJavaDeserialization() { + CordaRPCClient(rpcAddress).use(user.username, user.password) { connection -> + connection.proxy + rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow() + } + } driver { - val user = User("u", "p", setOf(startFlow())) - val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress). - start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue - assertThatThrownBy { future.getOrThrow() } - .isInstanceOf(CordaRuntimeException::class.java) + val devModeNode = startNode(params).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownBy { devModeNode.attemptJavaDeserialization() }.isInstanceOf(CordaRuntimeException::class.java) + assertThatThrownBy { node.attemptJavaDeserialization() }.isInstanceOf(InternalNodeException::class.java) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 5dc5749c55..aff9765263 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -11,9 +11,7 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode -import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.random63BitValue -import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.ParametersUpdateInfo @@ -21,8 +19,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.SignedNetworkParameters @@ -35,9 +31,9 @@ import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.internal.CompatibilityZoneParams -import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.network.NetworkMapServer +import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.* @@ -235,20 +231,4 @@ class NetworkMapTest : IntegrationTest() { } assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) } -} - -private fun DriverDSLImpl.startNode(providedName: CordaX500Name, devMode: Boolean): CordaFuture { - var customOverrides = emptyMap() - if (!devMode) { - val nodeDir = baseDirectory(providedName) - val nodeSslConfig = object : NodeSSLConfiguration { - override val baseDirectory = nodeDir - override val keyStorePassword = "cordacadevpass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail = true - } - nodeSslConfig.configureDevKeyAndTrustStores(providedName) - customOverrides = mapOf("devMode" to "false") - } - return startNode(providedName = providedName, customOverrides = customOverrides) -} +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt index d6372e2e7c..012a6b1345 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt @@ -3,24 +3,33 @@ package net.corda.node.services.rpc import co.paralleluniverse.fibers.Suspendable import net.corda.ClientRelevantException import net.corda.core.CordaRuntimeException -import net.corda.core.flows.* +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.User +import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.hibernate.exception.GenericJDBCException @@ -40,70 +49,91 @@ class RpcExceptionHandlingTest : IntegrationTest() { private val users = listOf(user) @Test - fun `rpc client handles exceptions thrown on node side`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + fun `rpc client receive client-relevant exceptions regardless of devMode`() { + val params = NodeParameters(rpcUsers = users) + val clientRelevantMessage = "This is for the players!" - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - - assertThatThrownBy { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> - - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - } + fun NodeHandle.throwExceptionFromFlow() { + rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() } - } - @Test - fun `rpc client handles client-relevant exceptions thrown on node side`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - val clientRelevantMessage = "This is for the players!" - - assertThatThrownBy { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> + fun assertThatThrownExceptionIsReceivedUnwrapped(node: NodeHandle) { + assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception -> assertThat(exception).hasNoCause() assertThat(exception.stackTrace).isEmpty() assertThat(exception.message).isEqualTo(clientRelevantMessage) } } + + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + val devModeNode = startNode(params).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownExceptionIsReceivedUnwrapped(devModeNode) + assertThatThrownExceptionIsReceivedUnwrapped(node) + } } @Test - fun `FlowException is received by the RPC client`() { + fun `FlowException is received by the RPC client only if in devMode`() { + val params = NodeParameters(rpcUsers = users) + val expectedMessage = "Flow error!" + val expectedErrorId = 123L + + fun NodeHandle.throwExceptionFromFlow() { + rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow() + } + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - val exceptionMessage = "Flow error!" - assertThatThrownBy { node.rpc.startFlow(::FlowExceptionFlow, exceptionMessage).returnValue.getOrThrow() } - .isInstanceOfSatisfying(FlowException::class.java) { exception -> - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - assertThat(exception.message).isEqualTo(exceptionMessage) - } + val devModeNode = startNode(params).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownBy { devModeNode.throwExceptionFromFlow() }.isInstanceOfSatisfying(FlowException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(expectedMessage) + assertThat(exception.errorId).isEqualTo(expectedErrorId) + } + assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.message) + assertThat(exception.errorId).isEqualTo(expectedErrorId) + } } } @Test fun `rpc client handles exceptions thrown on counter-party side`() { + val params = NodeParameters(rpcUsers = users) + + fun DriverDSL.scenario(devMode: Boolean) { + + val nodeA = startNode(ALICE_NAME, devMode, params).getOrThrow() + val nodeB = startNode(BOB_NAME, devMode, params).getOrThrow() + + nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() + } + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - val nodeA = startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = users)).getOrThrow() - val nodeB = startNode(NodeParameters(providedName = BOB_NAME, rpcUsers = users)).getOrThrow() - - assertThatThrownBy { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> + assertThatThrownBy { scenario(true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> assertThat(exception).hasNoCause() assertThat(exception.stackTrace).isEmpty() } } - } -} + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + assertThatThrownBy { scenario(false) }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> -@StartableByRPC -class Flow : FlowLogic() { - @Suspendable - override fun call(): String { - throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.message) + } + } } } @@ -133,7 +163,11 @@ class ClientRelevantErrorFlow(private val message: String) : FlowLogic() } @StartableByRPC -class FlowExceptionFlow(private val message: String) : FlowLogic() { +class FlowExceptionFlow(private val message: String, private val errorId: Long? = null) : FlowLogic() { @Suspendable - override fun call(): String = throw FlowException(message) -} + override fun call(): String { + val exception = FlowException(message) + errorId?.let { exception.originalErrorId = it } + throw exception + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b3c07bba49..fcca092dc6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -75,6 +75,7 @@ import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy +import net.corda.node.internal.rpc.proxies.ExceptionMaskingRpcOpsProxy import net.corda.node.internal.rpc.proxies.ExceptionSerialisingRpcOpsProxy import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.ContractUpgradeHandler @@ -251,8 +252,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, open fun makeRPCOps(flowStarter: FlowStarter, database: CordaPersistence, smm: StateMachineManager): CordaRPCOps { val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter, { shutdownExecutor.submit { stop() } }) + val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>() // Mind that order is relevant here. - val proxies = listOf<(CordaRPCOps) -> CordaRPCOps>(::AuthenticatedRpcOpsProxy, { it -> ExceptionSerialisingRpcOpsProxy(it, true) }) + proxies += ::AuthenticatedRpcOpsProxy + if (!configuration.devMode) { + proxies += { it -> ExceptionMaskingRpcOpsProxy(it, true) } + } + proxies += { it -> ExceptionSerialisingRpcOpsProxy(it, configuration.devMode) } return proxies.fold(ops) { delegate, decorate -> decorate(delegate) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt new file mode 100644 index 0000000000..ed5a883ec0 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt @@ -0,0 +1,135 @@ +/* + * 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.internal.rpc.proxies + +import net.corda.core.ClientRelevantError +import net.corda.core.CordaException +import net.corda.core.CordaRuntimeException +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.doOnError +import net.corda.core.flows.IdentifiableException +import net.corda.core.internal.concurrent.doOnError +import net.corda.core.internal.concurrent.mapError +import net.corda.core.internal.declaredField +import net.corda.core.mapErrors +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowHandleImpl +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.messaging.FlowProgressHandleImpl +import net.corda.core.utilities.loggerFor +import net.corda.node.internal.InvocationHandlerTemplate +import net.corda.nodeapi.exceptions.InternalNodeException +import rx.Observable +import java.lang.reflect.Method +import java.lang.reflect.Proxy.newProxyInstance +import kotlin.reflect.KClass + +internal class ExceptionMaskingRpcOpsProxy(private val delegate: CordaRPCOps, doLog: Boolean) : CordaRPCOps by proxy(delegate, doLog) { + private companion object { + private val logger = loggerFor() + + private val whitelist = setOf( + ClientRelevantError::class, + TransactionVerificationException::class + ) + + private fun proxy(delegate: CordaRPCOps, doLog: Boolean): CordaRPCOps { + val handler = ErrorObfuscatingInvocationHandler(delegate, whitelist, doLog) + return newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps + } + } + + private class ErrorObfuscatingInvocationHandler(override val delegate: CordaRPCOps, private val whitelist: Set>, private val doLog: Boolean) : InvocationHandlerTemplate { + override fun invoke(proxy: Any, method: Method, arguments: Array?): Any? { + try { + val result = super.invoke(proxy, method, arguments) + return result?.let { obfuscateResult(it) } + } catch (exception: Exception) { + // In this special case logging and re-throwing is the right approach. + log(exception) + throw obfuscate(exception) + } + } + + private fun obfuscateResult(result: RESULT): Any { + return when (result) { + is CordaFuture<*> -> wrapFuture(result) + is DataFeed<*, *> -> wrapFeed(result) + is FlowProgressHandle<*> -> wrapFlowProgressHandle(result) + is FlowHandle<*> -> wrapFlowHandle(result) + is Observable<*> -> wrapObservable(result) + else -> result + } + } + + private fun wrapFlowProgressHandle(handle: FlowProgressHandle<*>): FlowProgressHandle<*> { + val returnValue = wrapFuture(handle.returnValue) + val progress = wrapObservable(handle.progress) + val stepsTreeIndexFeed = handle.stepsTreeIndexFeed?.let { wrapFeed(it) } + val stepsTreeFeed = handle.stepsTreeFeed?.let { wrapFeed(it) } + + return FlowProgressHandleImpl(handle.id, returnValue, progress, stepsTreeIndexFeed, stepsTreeFeed) + } + + private fun wrapFlowHandle(handle: FlowHandle<*>): FlowHandle<*> { + return FlowHandleImpl(handle.id, wrapFuture(handle.returnValue)) + } + + private fun wrapObservable(observable: Observable): Observable { + return observable.doOnError(::log).mapErrors(::obfuscate) + } + + private fun wrapFeed(feed: DataFeed): DataFeed { + return feed.doOnError(::log).mapErrors(::obfuscate) + } + + private fun wrapFuture(future: CordaFuture): CordaFuture { + return future.doOnError(::log).mapError(::obfuscate) + } + + private fun log(error: Throwable) { + if (doLog) { + logger.error("Error during RPC invocation", error) + } + } + + private fun obfuscate(error: Throwable): Throwable { + val exposed = if (error.isWhitelisted()) error else InternalNodeException((error as? IdentifiableException)?.errorId) + removeDetails(exposed) + return exposed + } + + private fun removeDetails(error: Throwable) { + error.stackTrace = arrayOf() + error.declaredField("cause").value = null + error.declaredField("suppressedExceptions").value = null + when (error) { + is CordaException -> error.setCause(null) + is CordaRuntimeException -> error.setCause(null) + } + } + + private fun Throwable.isWhitelisted(): Boolean { + return whitelist.any { it.isInstance(this) } + } + + override fun toString(): String { + return "ErrorObfuscatingInvocationHandler(whitelist=$whitelist)" + } + } + + override fun toString(): String { + return "ExceptionMaskingRpcOpsProxy" + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 7f7839d8c5..8d24c56a5f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -42,6 +42,7 @@ import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook +import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.crypto.X509KeyStore @@ -1126,3 +1127,20 @@ fun writeConfig(path: Path, filename: String, config: Config) { private fun Config.toNodeOnly(): Config { return if (hasPath("webAddress")) withoutPath("webAddress").withoutPath("useHTTPS") else this } + +fun DriverDSL.startNode(providedName: CordaX500Name, devMode: Boolean, parameters: NodeParameters = NodeParameters()): CordaFuture { + var customOverrides = emptyMap() + if (!devMode) { + val nodeDir = baseDirectory(providedName) + val nodeSslConfig = object : NodeSSLConfiguration { + override val baseDirectory = nodeDir + override val keyStorePassword = "cordacadevpass" + override val trustStorePassword = "trustpass" + override val crlCheckSoftFail = true + } + nodeSslConfig.configureDevKeyAndTrustStores(providedName) + customOverrides = mapOf("devMode" to "false") + } + return startNode(parameters, providedName = providedName, customOverrides = customOverrides) +} +