[CORDA-1264]: Mask internal errors if devMode is false. (#871)

This commit is contained in:
Michele Sollecito 2018-05-23 09:55:24 +01:00 committed by GitHub
parent d9510d9a22
commit cafcecec2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 275 additions and 76 deletions

View File

@ -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 * 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. * 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 { CordaException(message, cause), IdentifiableException {
constructor(message: String?, cause: Throwable?) : this(message, cause, null)
constructor(message: String?) : this(message, null) constructor(message: String?) : this(message, null)
constructor(cause: Throwable?) : this(cause?.toString(), cause) constructor(cause: Throwable?) : this(cause?.toString(), cause)
constructor() : this(null, null) constructor() : this(null, null)
var originalErrorId: Long? = null
override fun getErrorId(): Long? = originalErrorId override fun getErrorId(): Long? = originalErrorId
} }
// DOCEND 1 // DOCEND 1

View File

@ -30,6 +30,7 @@ import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashIssueFlow
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow 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.DummyContract
import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
@ -147,6 +148,7 @@ class ContractUpgradeFlowTest {
startFlow<ContractUpgradeFlow.Authorise>(), startFlow<ContractUpgradeFlow.Authorise>(),
startFlow<ContractUpgradeFlow.Deauthorise>() startFlow<ContractUpgradeFlow.Deauthorise>()
)) ))
val expectedExceptionClass = if (aliceNode.internals.configuration.devMode) CordaRuntimeException::class else InternalNodeException::class
val rpcA = startProxy(aliceNode, user) val rpcA = startProxy(aliceNode, user)
val rpcB = startProxy(bobNode, user) val rpcB = startProxy(bobNode, user)
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bob)) val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bob))
@ -163,7 +165,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(CordaRuntimeException::class) { rejectedFuture.getOrThrow() } assertFailsWith(expectedExceptionClass) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade, and immediately deauthorise the same. // Party B authorise the contract state upgrade, and immediately deauthorise the same.
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },
@ -178,7 +180,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(CordaRuntimeException::class) { deauthorisedFuture.getOrThrow() } assertFailsWith(expectedExceptionClass) { deauthorisedFuture.getOrThrow() }
// Party B authorise the contract state upgrade. // Party B authorise the contract state upgrade.
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) },

View File

@ -10,6 +10,8 @@ Unreleased
* ``WireTransaction.Companion.createComponentGroups`` has been marked as ``@CordaInternal``. It was never intended to be * ``WireTransaction.Companion.createComponentGroups`` has been marked as ``@CordaInternal``. It was never intended to be
public and was already internal for Kotlin code. 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 * 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. of ``Kryo`` from general use within Corda, remaining only for use in flow checkpointing.

View File

@ -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 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. 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. 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 Connection management
--------------------- ---------------------
It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()`` It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()``

View File

@ -13,6 +13,7 @@ package net.corda.node
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div 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.core.utilities.getOrThrow
import net.corda.node.internal.NodeStartup import net.corda.node.internal.NodeStartup
import net.corda.node.services.Permissions.Companion.startFlow 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.common.internal.ProjectStructure.projectRootDir
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.driver.DriverParameters 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.driver.driver
import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTest
import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.node.User 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.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.ClassRule import org.junit.ClassRule
import org.junit.Test 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 import kotlin.test.assertEquals
class BootTests : IntegrationTest() { class BootTests : IntegrationTest() {
@ -50,12 +58,21 @@ class BootTests : IntegrationTest() {
@Test @Test
fun `java deserialization is disabled`() { fun `java deserialization is disabled`() {
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>()))
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 { driver {
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>())) val devModeNode = startNode(params).getOrThrow()
val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress). val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue
assertThatThrownBy { future.getOrThrow() } assertThatThrownBy { devModeNode.attemptJavaDeserialization() }.isInstanceOf(CordaRuntimeException::class.java)
.isInstanceOf(CordaRuntimeException::class.java) assertThatThrownBy { node.attemptJavaDeserialization() }.isInstanceOf(InternalNodeException::class.java)
} }
} }

View File

@ -11,9 +11,7 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.ParametersUpdateInfo 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.serialization.serialize
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds 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_FILE_NAME
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters 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.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.node.internal.CompatibilityZoneParams 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.internalDriver
import net.corda.testing.node.internal.network.NetworkMapServer 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.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.* import org.junit.*
@ -236,19 +232,3 @@ class NetworkMapTest : IntegrationTest() {
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
} }
} }
private fun DriverDSLImpl.startNode(providedName: CordaX500Name, devMode: Boolean): CordaFuture<NodeHandle> {
var customOverrides = emptyMap<String, String>()
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)
}

View File

@ -3,24 +3,33 @@ package net.corda.node.services.rpc
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.ClientRelevantException import net.corda.ClientRelevantException
import net.corda.core.CordaRuntimeException 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.identity.Party
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
import net.corda.nodeapi.exceptions.InternalNodeException
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTest
import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.node.User 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.Assertions.assertThatThrownBy
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
import org.hibernate.exception.GenericJDBCException import org.hibernate.exception.GenericJDBCException
@ -40,70 +49,91 @@ class RpcExceptionHandlingTest : IntegrationTest() {
private val users = listOf(user) private val users = listOf(user)
@Test @Test
fun `rpc client handles exceptions thrown on node side`() { fun `rpc client receive client-relevant exceptions regardless of devMode`() {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val params = NodeParameters(rpcUsers = users)
val clientRelevantMessage = "This is for the players!"
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() fun NodeHandle.throwExceptionFromFlow() {
rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow()
assertThatThrownBy { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty()
}
} }
}
@Test fun assertThatThrownExceptionIsReceivedUnwrapped(node: NodeHandle) {
fun `rpc client handles client-relevant exceptions thrown on node side`() { assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception ->
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 ->
assertThat(exception).hasNoCause() assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception.stackTrace).isEmpty()
assertThat(exception.message).isEqualTo(clientRelevantMessage) 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 @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())) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() val devModeNode = startNode(params).getOrThrow()
val exceptionMessage = "Flow error!" val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow()
assertThatThrownBy { node.rpc.startFlow(::FlowExceptionFlow, exceptionMessage).returnValue.getOrThrow() }
.isInstanceOfSatisfying(FlowException::class.java) { exception -> assertThatThrownBy { devModeNode.throwExceptionFromFlow() }.isInstanceOfSatisfying(FlowException::class.java) { exception ->
assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception).hasNoCause()
assertThat(exception.message).isEqualTo(exceptionMessage) 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 @Test
fun `rpc client handles exceptions thrown on counter-party side`() { 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())) { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
val nodeA = startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = users)).getOrThrow() assertThatThrownBy { scenario(true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception ->
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 ->
assertThat(exception).hasNoCause() assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception.stackTrace).isEmpty()
} }
} }
} driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
} assertThatThrownBy { scenario(false) }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
@StartableByRPC assertThat(exception).hasNoCause()
class Flow : FlowLogic<String>() { assertThat(exception.stackTrace).isEmpty()
@Suspendable assertThat(exception.message).isEqualTo(InternalNodeException.message)
override fun call(): String { }
throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) }
} }
} }
@ -133,7 +163,11 @@ class ClientRelevantErrorFlow(private val message: String) : FlowLogic<String>()
} }
@StartableByRPC @StartableByRPC
class FlowExceptionFlow(private val message: String) : FlowLogic<String>() { class FlowExceptionFlow(private val message: String, private val errorId: Long? = null) : FlowLogic<String>() {
@Suspendable @Suspendable
override fun call(): String = throw FlowException(message) override fun call(): String {
val exception = FlowException(message)
errorId?.let { exception.originalErrorId = it }
throw exception
}
} }

View File

@ -75,6 +75,7 @@ import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy 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.rpc.proxies.ExceptionSerialisingRpcOpsProxy
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.ContractUpgradeHandler 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 { open fun makeRPCOps(flowStarter: FlowStarter, database: CordaPersistence, smm: StateMachineManager): CordaRPCOps {
val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter, { shutdownExecutor.submit { stop() } }) val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter, { shutdownExecutor.submit { stop() } })
val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>()
// Mind that order is relevant here. // 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) } return proxies.fold(ops) { delegate, decorate -> decorate(delegate) }
} }

View File

@ -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<ExceptionMaskingRpcOpsProxy>()
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<KClass<*>>, private val doLog: Boolean) : InvocationHandlerTemplate {
override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): 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 <RESULT : Any> 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 <ELEMENT> wrapObservable(observable: Observable<ELEMENT>): Observable<ELEMENT> {
return observable.doOnError(::log).mapErrors(::obfuscate)
}
private fun <SNAPSHOT, ELEMENT> wrapFeed(feed: DataFeed<SNAPSHOT, ELEMENT>): DataFeed<SNAPSHOT, ELEMENT> {
return feed.doOnError(::log).mapErrors(::obfuscate)
}
private fun <RESULT> wrapFuture(future: CordaFuture<RESULT>): CordaFuture<RESULT> {
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<StackTraceElement>()
error.declaredField<Any?>("cause").value = null
error.declaredField<Any?>("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"
}
}

View File

@ -42,6 +42,7 @@ import net.corda.node.utilities.registration.NodeRegistrationHelper
import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.addShutdownHook 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.parseAs
import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
@ -1126,3 +1127,20 @@ fun writeConfig(path: Path, filename: String, config: Config) {
private fun Config.toNodeOnly(): Config { private fun Config.toNodeOnly(): Config {
return if (hasPath("webAddress")) withoutPath("webAddress").withoutPath("useHTTPS") else this return if (hasPath("webAddress")) withoutPath("webAddress").withoutPath("useHTTPS") else this
} }
fun DriverDSL.startNode(providedName: CordaX500Name, devMode: Boolean, parameters: NodeParameters = NodeParameters()): CordaFuture<NodeHandle> {
var customOverrides = emptyMap<String, String>()
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)
}