Merge remote-tracking branch 'open/master' into colljos-os-merge-rc01

This commit is contained in:
josecoll
2017-12-18 10:24:38 +00:00
215 changed files with 4624 additions and 1717 deletions

View File

@ -0,0 +1,242 @@
package net.corda.node.amqp
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.internal.div
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.toBase58String
import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.config.*
import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertArrayEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import rx.Observable
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
class AMQPBridgeTest {
@Rule
@JvmField
val temporaryFolder = TemporaryFolder()
private val ALICE = TestIdentity(ALICE_NAME)
private val BOB = TestIdentity(BOB_NAME)
private val artemisPort = freePort()
private val artemisPort2 = freePort()
private val amqpPort = freePort()
private val artemisAddress = NetworkHostAndPort("localhost", artemisPort)
private val artemisAddress2 = NetworkHostAndPort("localhost", artemisPort2)
private val amqpAddress = NetworkHostAndPort("localhost", amqpPort)
private abstract class AbstractNodeConfiguration : NodeConfiguration
@Test
fun `test acked and nacked messages`() {
// Create local queue
val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String()
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
// Pre-populate local queue with 3 messages
val artemis = artemisClient.started!!
for (i in 0 until 3) {
val artemisMessage = artemis.session.createMessage(true).apply {
putIntProperty("CountProp", i)
writeBodyBufferBytes("Test$i".toByteArray())
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
}
artemis.producer.send(sourceQueueName, artemisMessage)
}
//Create target server
val amqpServer = createAMQPServer()
val receive = amqpServer.onReceive.toBlocking().iterator
amqpServer.start()
val received1 = receive.next()
val messageID1 = received1.applicationProperties["CountProp"] as Int
assertArrayEquals("Test$messageID1".toByteArray(), received1.payload)
assertEquals(0, messageID1)
received1.complete(true) // Accept first message
val received2 = receive.next()
val messageID2 = received2.applicationProperties["CountProp"] as Int
assertArrayEquals("Test$messageID2".toByteArray(), received2.payload)
assertEquals(1, messageID2)
received2.complete(false) // Reject message
while (true) {
val received3 = receive.next()
val messageID3 = received3.applicationProperties["CountProp"] as Int
assertArrayEquals("Test$messageID3".toByteArray(), received3.payload)
assertNotEquals(0, messageID3)
if (messageID3 != 1) { // keep rejecting any batched items following rejection
received3.complete(false)
} else { // beginnings of replay so accept again
received3.complete(true)
break
}
}
while (true) {
val received4 = receive.next()
val messageID4 = received4.applicationProperties["CountProp"] as Int
assertArrayEquals("Test$messageID4".toByteArray(), received4.payload)
if (messageID4 != 1) { // we may get a duplicate of the rejected message, in which case skip
assertEquals(2, messageID4) // next message should be in order though
break
}
received4.complete(true)
}
// Send a fresh item and check receive
val artemisMessage = artemis.session.createMessage(true).apply {
putIntProperty("CountProp", -1)
writeBodyBufferBytes("Test_end".toByteArray())
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
}
artemis.producer.send(sourceQueueName, artemisMessage)
val received5 = receive.next()
val messageID5 = received5.applicationProperties["CountProp"] as Int
assertArrayEquals("Test_end".toByteArray(), received5.payload)
assertEquals(-1, messageID5) // next message should be in order
received5.complete(true)
amqpServer.stop()
artemisClient.stop()
artemisServer.stop()
}
@Test
fun `Test legacy bridge still works`() {
// Create local queue
val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String()
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
val (artemisServer, artemisClient) = createArtemis(null)
val artemis = artemisLegacyClient.started!!
for (i in 0 until 3) {
val artemisMessage = artemis.session.createMessage(true).apply {
putIntProperty("CountProp", i)
writeBodyBufferBytes("Test$i".toByteArray())
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
}
artemis.producer.send(sourceQueueName, artemisMessage)
}
val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
for (i in 0 until 3) {
val msg = subs.receive()
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
assertArrayEquals("Test$i".toByteArray(), messageBody)
assertEquals(i, msg.getIntProperty("CountProp"))
}
artemisClient.stop()
artemisServer.stop()
artemisLegacyClient.stop()
artemisLegacyServer.stop()
}
private fun createArtemis(sourceQueueName: String?): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
doReturn(ALICE_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn("").whenever(it).exportJMXto
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(true).whenever(it).useAMQPBridges
}
artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also {
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
}
val userService = rigorousMock<RPCSecurityManager>()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
artemisServer.start()
artemisClient.start()
val artemis = artemisClient.started!!
if (sourceQueueName != null) {
// Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
}
return Pair(artemisServer, artemisClient)
}
private fun createLegacyArtemis(sourceQueueName: String): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "artemis2").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn("").whenever(it).exportJMXto
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(false).whenever(it).useAMQPBridges
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
}
artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also {
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
}
val userService = rigorousMock<RPCSecurityManager>()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress2, MAX_MESSAGE_SIZE)
artemisServer.start()
artemisClient.start()
val artemis = artemisClient.started!!
// Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
return Pair(artemisServer, artemisClient)
}
private fun createAMQPServer(): AMQPServer {
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
}
serverConfig.configureWithDevSSLCertificate()
val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword)
val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword)
val amqpServer = AMQPServer("0.0.0.0",
amqpPort,
ArtemisMessagingComponent.PEER_USER,
ArtemisMessagingComponent.PEER_USER,
serverKeystore,
serverConfig.keyStorePassword,
serverTruststore,
trace = true)
return amqpServer
}
}

View File

@ -0,0 +1,309 @@
package net.corda.node.amqp
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.toFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.netty.AMQPClient
import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.RoutingType
import org.junit.Assert.assertArrayEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import rx.Observable.never
import kotlin.test.assertEquals
class ProtonWrapperTests {
@Rule
@JvmField
val temporaryFolder = TemporaryFolder()
private val serverPort = freePort()
private val serverPort2 = freePort()
private val artemisPort = freePort()
private abstract class AbstractNodeConfiguration : NodeConfiguration
@Test
fun `Simple AMPQ Client to Server`() {
val amqpServer = createServer(serverPort)
amqpServer.use {
amqpServer.start()
val receiveSubs = amqpServer.onReceive.subscribe {
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
assertEquals("p2p.inbound", it.topic)
assertEquals("Test", String(it.payload))
it.complete(true)
}
val amqpClient = createClient()
amqpClient.use {
val serverConnected = amqpServer.onConnection.toFuture()
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
val serverConnect = serverConnected.get()
assertEquals(true, serverConnect.connected)
assertEquals(BOB_NAME, CordaX500Name.parse(serverConnect.remoteCert!!.subject.toString()))
val clientConnect = clientConnected.get()
assertEquals(true, clientConnect.connected)
assertEquals(ALICE_NAME, CordaX500Name.parse(clientConnect.remoteCert!!.subject.toString()))
val msg = amqpClient.createMessage("Test".toByteArray(),
"p2p.inbound",
ALICE_NAME.toString(),
emptyMap())
amqpClient.write(msg)
assertEquals(MessageStatus.Acknowledged, msg.onComplete.get())
receiveSubs.unsubscribe()
}
}
}
@Test
fun `AMPQ Client refuses to connect to unexpected server`() {
val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"))
amqpServer.use {
amqpServer.start()
val amqpClient = createClient()
amqpClient.use {
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
val clientConnect = clientConnected.get()
assertEquals(false, clientConnect.connected)
}
}
}
@Test
fun `Client Failover for multiple IP`() {
val amqpServer = createServer(serverPort)
val amqpServer2 = createServer(serverPort2)
val amqpClient = createClient()
try {
val serverConnected = amqpServer.onConnection.toFuture()
val serverConnected2 = amqpServer2.onConnection.toFuture()
val clientConnected = amqpClient.onConnection.toBlocking().iterator
amqpServer.start()
amqpClient.start()
val serverConn1 = serverConnected.get()
assertEquals(true, serverConn1.connected)
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn1.remoteCert!!.subject.toString()))
val connState1 = clientConnected.next()
assertEquals(true, connState1.connected)
assertEquals(ALICE_NAME, CordaX500Name.parse(connState1.remoteCert!!.subject.toString()))
assertEquals(serverPort, connState1.remoteAddress.port)
// Fail over
amqpServer2.start()
amqpServer.stop()
val connState2 = clientConnected.next()
assertEquals(false, connState2.connected)
assertEquals(serverPort, connState2.remoteAddress.port)
val serverConn2 = serverConnected2.get()
assertEquals(true, serverConn2.connected)
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn2.remoteCert!!.subject.toString()))
val connState3 = clientConnected.next()
assertEquals(true, connState3.connected)
assertEquals(ALICE_NAME, CordaX500Name.parse(connState3.remoteCert!!.subject.toString()))
assertEquals(serverPort2, connState3.remoteAddress.port)
// Fail back
amqpServer.start()
amqpServer2.stop()
val connState4 = clientConnected.next()
assertEquals(false, connState4.connected)
assertEquals(serverPort2, connState4.remoteAddress.port)
val serverConn3 = serverConnected.get()
assertEquals(true, serverConn3.connected)
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn3.remoteCert!!.subject.toString()))
val connState5 = clientConnected.next()
assertEquals(true, connState5.connected)
assertEquals(ALICE_NAME, CordaX500Name.parse(connState5.remoteCert!!.subject.toString()))
assertEquals(serverPort, connState5.remoteAddress.port)
} finally {
amqpClient.close()
amqpServer.close()
amqpServer2.close()
}
}
@Test
fun `Send a message from AMQP to Artemis inbox`() {
val (server, artemisClient) = createArtemisServerAndClient()
val amqpClient = createClient()
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.parse(clientConnected.get().remoteCert!!.subject.toString()))
val artemis = artemisClient.started!!
val sendAddress = "p2p.inbound"
artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true)
val consumer = artemis.session.createConsumer("queue")
val testData = "Test".toByteArray()
val testProperty = mutableMapOf<Any?, Any?>()
testProperty["TestProp"] = "1"
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message)
assertEquals(MessageStatus.Acknowledged, message.onComplete.get())
val received = consumer.receive()
assertEquals("1", received.getStringProperty("TestProp"))
assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) })
amqpClient.stop()
artemisClient.stop()
server.stop()
}
@Test
fun `shared AMQPClient threadpool tests`() {
val amqpServer = createServer(serverPort)
amqpServer.use {
val connectionEvents = amqpServer.onConnection.toBlocking().iterator
amqpServer.start()
val sharedThreads = NioEventLoopGroup()
val amqpClient1 = createSharedThreadsClient(sharedThreads, 0)
val amqpClient2 = createSharedThreadsClient(sharedThreads, 1)
amqpClient1.start()
val connection1 = connectionEvents.next()
assertEquals(true, connection1.connected)
val connection1ID = CordaX500Name.parse(connection1.remoteCert!!.subject.toString())
assertEquals("client 0", connection1ID.organisationUnit)
val source1 = connection1.remoteAddress
amqpClient2.start()
val connection2 = connectionEvents.next()
assertEquals(true, connection2.connected)
val connection2ID = CordaX500Name.parse(connection2.remoteCert!!.subject.toString())
assertEquals("client 1", connection2ID.organisationUnit)
val source2 = connection2.remoteAddress
// Stopping one shouldn't disconnect the other
amqpClient1.stop()
val connection3 = connectionEvents.next()
assertEquals(false, connection3.connected)
assertEquals(source1, connection3.remoteAddress)
assertEquals(false, amqpClient1.connected)
assertEquals(true, amqpClient2.connected)
// Now shutdown both
amqpClient2.stop()
val connection4 = connectionEvents.next()
assertEquals(false, connection4.connected)
assertEquals(source2, connection4.remoteAddress)
assertEquals(false, amqpClient1.connected)
assertEquals(false, amqpClient2.connected)
// Now restarting one should work
amqpClient1.start()
val connection5 = connectionEvents.next()
assertEquals(true, connection5.connected)
val connection5ID = CordaX500Name.parse(connection5.remoteCert!!.subject.toString())
assertEquals("client 0", connection5ID.organisationUnit)
assertEquals(true, amqpClient1.connected)
assertEquals(false, amqpClient2.connected)
// Cleanup
amqpClient1.stop()
sharedThreads.shutdownGracefully()
sharedThreads.terminationFuture().sync()
}
}
private fun createArtemisServerAndClient(): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
doReturn(CHARLIE_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn("").whenever(it).exportJMXto
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(true).whenever(it).useAMQPBridges
}
artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also {
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
}
val userService = rigorousMock<RPCSecurityManager>()
val server = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), MAX_MESSAGE_SIZE)
server.start()
client.start()
return Pair(server, client)
}
private fun createClient(): AMQPClient {
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
}
clientConfig.configureWithDevSSLCertificate()
val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword)
val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword)
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort),
NetworkHostAndPort("localhost", serverPort2),
NetworkHostAndPort("localhost", artemisPort)),
setOf(ALICE_NAME, CHARLIE_NAME),
PEER_USER,
PEER_USER,
clientKeystore,
clientConfig.keyStorePassword,
clientTruststore, true)
return amqpClient
}
private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int): AMQPClient {
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory
doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
}
clientConfig.configureWithDevSSLCertificate()
val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword)
val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword)
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)),
setOf(ALICE_NAME),
PEER_USER,
PEER_USER,
clientKeystore,
clientConfig.keyStorePassword,
clientTruststore, true, sharedEventGroup)
return amqpClient
}
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME): AMQPServer {
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
doReturn(name).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
}
serverConfig.configureWithDevSSLCertificate()
val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword)
val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword)
val amqpServer = AMQPServer("0.0.0.0",
port,
PEER_USER,
PEER_USER,
serverKeystore,
serverConfig.keyStorePassword,
serverTruststore)
return amqpServer
}
}

View File

@ -24,15 +24,21 @@ import net.corda.testing.*
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.services.MockAttachmentStorage
import net.corda.testing.internal.rigorousMock
import org.junit.Assert.assertEquals
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.net.URLClassLoader
import java.nio.file.Files
import kotlin.test.assertFailsWith
class AttachmentLoadingTests : IntegrationTest() {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
private val cordapp get() = provider.cordapps.first()
@ -83,7 +89,7 @@ class AttachmentLoadingTests : IntegrationTest() {
}
@Test
fun `test a wire transaction has loaded the correct attachment`() = withTestSerialization {
fun `test a wire transaction has loaded the correct attachment`() {
val appClassLoader = appContext.classLoader
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
@ -99,7 +105,7 @@ class AttachmentLoadingTests : IntegrationTest() {
}
@Test
fun `test that attachments retrieved over the network are not used for code`() {
fun `test that attachments retrieved over the network are not used for code`() = withoutTestSerialization {
driver {
installIsolatedCordappTo(bankAName)
val (bankA, bankB) = createTwoNodes()
@ -107,15 +113,17 @@ class AttachmentLoadingTests : IntegrationTest() {
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
}
Unit
}
@Test
fun `tests that if the attachment is loaded on both sides already that a flow can run`() {
fun `tests that if the attachment is loaded on both sides already that a flow can run`() = withoutTestSerialization {
driver {
installIsolatedCordappTo(bankAName)
installIsolatedCordappTo(bankBName)
val (bankA, bankB) = createTwoNodes()
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
Unit
}
}

View File

@ -24,10 +24,12 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.nodeapi.internal.NetworkParametersCopier
import net.corda.nodeapi.internal.NotaryInfo
import net.corda.nodeapi.internal.ServiceIdentityGenerator
import net.corda.testing.*
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.testing.chooseIdentity
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.testing.IntegrationTest
import net.corda.testing.IntegrationTestSchemas
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockNetwork
@ -55,7 +57,7 @@ class BFTNotaryServiceTests : IntegrationTest() {
@Before
fun before() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
node = mockNet.createNode()
}
@After

View File

@ -1,26 +1,37 @@
package net.corda.node.services.network
import net.corda.core.crypto.SignedData
import net.corda.core.internal.list
import net.corda.core.internal.readAll
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.*
import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.internalDriver
import net.corda.testing.node.network.NetworkMapServer
import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.ClassRule
import org.junit.Test
import org.junit.*
import java.net.URL
import kotlin.test.assertEquals
class NetworkMapTest : IntegrationTest() {
companion object {
@ClassRule @JvmField
@ClassRule
@JvmField
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(),
DUMMY_NOTARY_NAME.toDatabaseSchemaName())
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val cacheTimeout = 1.seconds
private val portAllocation = PortAllocation.Incremental(10000)
@ -39,9 +50,22 @@ class NetworkMapTest : IntegrationTest() {
networkMapServer.close()
}
@Test
fun `node correctly downloads and saves network parameters file on startup`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val networkParameters = alice.configuration.baseDirectory
.list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() }
.readAll()
.deserialize<SignedData<NetworkParameters>>()
.verified()
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters)
}
}
@Test
fun `nodes can see each other using the http network map`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val bob = startNode(providedName = BOB_NAME)
val notaryNode = defaultNotaryNode.get()
@ -56,7 +80,7 @@ class NetworkMapTest : IntegrationTest() {
@Test
fun `nodes process network map add updates correctly when adding new node to network map`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
@ -77,7 +101,7 @@ class NetworkMapTest : IntegrationTest() {
@Test
fun `nodes process network map remove updates correctly`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val bob = startNode(providedName = BOB_NAME)
val notaryNode = defaultNotaryNode.get()

View File

@ -3,14 +3,15 @@ package net.corda.node.services.network
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import net.corda.cordform.CordformNode
import net.corda.core.crypto.SignedData
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.NodeInfoFilesCopier
import net.corda.testing.*
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.testing.ALICE_NAME
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import net.corda.testing.node.MockKeyManagementService
import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions.assertThat
@ -27,20 +28,20 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue
class NodeInfoWatcherTest {
private companion object {
val alice = TestIdentity(ALICE_NAME, 70)
val nodeInfo = NodeInfo(listOf(), listOf(alice.identity), 0, 0)
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var nodeInfoPath: Path
private val scheduler = TestScheduler()
private val testSubscriber = TestSubscriber<NodeInfo>()
private lateinit var nodeInfo: NodeInfo
private lateinit var signedNodeInfo: SignedNodeInfo
private lateinit var nodeInfoPath: Path
private lateinit var keyManagementService: KeyManagementService
// Object under test
@ -48,8 +49,11 @@ class NodeInfoWatcherTest {
@Before
fun start() {
val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME)
nodeInfo = nodeInfoAndSigned.first
signedNodeInfo = nodeInfoAndSigned.second
val identityService = makeTestIdentityService()
keyManagementService = MockKeyManagementService(identityService, alice.key)
keyManagementService = MockKeyManagementService(identityService)
nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler)
nodeInfoPath = tempFolder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY
}
@ -58,7 +62,6 @@ class NodeInfoWatcherTest {
fun `save a NodeInfo`() {
assertEquals(0,
tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size)
val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey))
NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo)
val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }
@ -74,7 +77,6 @@ class NodeInfoWatcherTest {
fun `save a NodeInfo to JimFs`() {
val jimFs = Jimfs.newFileSystem(Configuration.unix())
val jimFolder = jimFs.getPath("/nodeInfo")
val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey))
NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo)
}
@ -82,11 +84,9 @@ class NodeInfoWatcherTest {
fun `load an empty Directory`() {
nodeInfoPath.createDirectories()
val subscription = nodeInfoWatcher.nodeInfoUpdates()
.subscribe(testSubscriber)
val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber)
try {
advanceTime()
val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(0, readNodes.size)
} finally {
@ -96,15 +96,13 @@ class NodeInfoWatcherTest {
@Test
fun `load a non empty Directory`() {
createNodeInfoFileInPath(nodeInfo)
createNodeInfoFileInPath()
val subscription = nodeInfoWatcher.nodeInfoUpdates()
.subscribe(testSubscriber)
val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber)
advanceTime()
try {
val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(1, readNodes.size)
assertEquals(nodeInfo, readNodes.first())
} finally {
@ -117,14 +115,13 @@ class NodeInfoWatcherTest {
nodeInfoPath.createDirectories()
// Start polling with an empty folder.
val subscription = nodeInfoWatcher.nodeInfoUpdates()
.subscribe(testSubscriber)
val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber)
try {
// Ensure the watch service is started.
advanceTime()
// Check no nodeInfos are read.
assertEquals(0, testSubscriber.valueCount)
createNodeInfoFileInPath(nodeInfo)
createNodeInfoFileInPath()
advanceTime()
@ -143,8 +140,7 @@ class NodeInfoWatcherTest {
}
// Write a nodeInfo under the right path.
private fun createNodeInfoFileInPath(nodeInfo: NodeInfo) {
val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey))
private fun createNodeInfoFileInPath() {
NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo)
}
}

View File

@ -14,14 +14,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.testing.ALICE_NAME
import net.corda.testing.IntegrationTest
import net.corda.testing.IntegrationTestSchemas
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.internalDriver
import net.corda.testing.node.network.NetworkMapServer
import net.corda.testing.toDatabaseSchemaName
import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.pkcs.PKCS10CertificationRequest
@ -29,6 +28,7 @@ import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.After
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.InputStream
@ -47,7 +47,9 @@ class NodeRegistrationTest : IntegrationTest() {
@ClassRule @JvmField
val databaseSchemas = IntegrationTestSchemas("Alice")
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val portAllocation = PortAllocation.Incremental(13000)
private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
private val registrationHandler = RegistrationHandler(rootCertAndKeyPair)
@ -57,7 +59,7 @@ class NodeRegistrationTest : IntegrationTest() {
@Before
fun startServer() {
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), registrationHandler)
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler)
serverHostAndPort = server.start()
}
@ -74,7 +76,8 @@ class NodeRegistrationTest : IntegrationTest() {
internalDriver(
portAllocation = portAllocation,
notarySpecs = emptyList(),
compatibilityZone = compatibilityZone
compatibilityZone = compatibilityZone,
initialiseSerialization = false
) {
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
assertThat(registrationHandler.idsPolled).contains("Alice")

View File

@ -8,7 +8,6 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.*
import net.corda.testing.messaging.SimpleMQClient
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException

View File

@ -1,7 +1,6 @@
package net.corda.services.messaging
import net.corda.nodeapi.internal.config.User
import net.corda.testing.messaging.SimpleMQClient
import org.junit.Test
/**

View File

@ -26,7 +26,6 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREF
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.testing.*
import net.corda.testing.messaging.SimpleMQClient
import net.corda.testing.node.internal.NodeBasedTest
import net.corda.testing.node.startFlow
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException

View File

@ -101,13 +101,10 @@ class P2PMessagingTest : IntegrationTest() {
// Restart the node and expect a response
val aliceRestarted = startAlice()
val responseFuture = openFuture<Any>()
aliceRestarted.network.runOnNextMessage("test.response") {
val responseFuture = openFuture<Any>() aliceRestarted.network.runOnNextMessage("test.response") {
responseFuture.set(it.data.deserialize())
}
val response = responseFuture.getOrThrow()
assertThat(crashingNodes.requestsReceived.get()).isGreaterThan(numberOfRequestsReceived)
assertThat(response).isEqualTo(responseMessage)
}

View File

@ -0,0 +1,47 @@
package net.corda.services.messaging
import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.configureTestSSL
import org.apache.activemq.artemis.api.core.client.*
/**
* As the name suggests this is a simple client for connecting to MQ brokers.
*/
class SimpleMQClient(val target: NetworkHostAndPort,
private val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) {
companion object {
val DEFAULT_MQ_LEGAL_NAME = CordaX500Name(organisation = "SimpleMQClient", locality = "London", country = "GB")
}
lateinit var sessionFactory: ClientSessionFactory
lateinit var session: ClientSession
lateinit var producer: ClientProducer
fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) {
val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), target, config, enableSSL = enableSSL)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
isBlockOnNonDurableSend = true
threadPoolMaxSize = 1
isUseGlobalPools = nodeSerializationEnv != null
}
sessionFactory = locator.createSessionFactory()
session = sessionFactory.createSession(username, password, false, true, true, locator.isPreAcknowledge, locator.ackBatchSize)
session.start()
producer = session.createProducer()
}
fun createMessage(): ClientMessage = session.createMessage(false)
fun stop() {
try {
sessionFactory.close()
} catch (e: Exception) {
// sessionFactory might not have initialised.
}
}
}

View File

@ -10,6 +10,8 @@ import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sign
import net.corda.core.crypto.newSecureRandom
@ -33,10 +35,10 @@ import net.corda.node.internal.classloading.requireAnnotation
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.security.RPCSecurityManager
import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.api.*
import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NodeConfiguration
@ -60,7 +62,10 @@ import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.internal.NetworkParameters
import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
@ -139,11 +144,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected lateinit var network: MessagingService
protected val runOnStop = ArrayList<() -> Any?>()
protected val _nodeReadyFuture = openFuture<Unit>()
protected val networkMapClient: NetworkMapClient? by lazy {
configuration.compatibilityZoneURL?.let {
NetworkMapClient(it, services.identityService.trustRoot)
}
}
protected var networkMapClient: NetworkMapClient? = null
lateinit var securityManager: RPCSecurityManager get
@ -178,6 +179,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
validateKeystore()
}
private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
// For now we assume the node has only one identity (excluding any composite ones)
val owningKey = nodeInfo.legalIdentities.single { it.owningKey !is CompositeKey }.owningKey
val serialised = nodeInfo.serialize()
val signature = sign(owningKey, serialised)
return SignedNodeInfo(serialised, listOf(signature))
}
open fun generateNodeInfo() {
check(started == null) { "Node has already been started" }
log.info("Generating nodeInfo ...")
@ -189,11 +198,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
// a code smell.
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey }
val serialisedNodeInfo = info.serialize()
val signature = identityKeypair.sign(serialisedNodeInfo)
// TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature.
NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature))
val signedNodeInfo = signNodeInfo(info) { publicKey, serialised ->
val privateKey = keyPairs.single { it.public == publicKey }.private
privateKey.sign(serialised.bytes)
}
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
}
}
@ -213,10 +222,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
check(started == null) { "Node has already been started" }
log.info("Node starting up ...")
initCertificate()
readNetworkParameters()
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val identityService = makeIdentityService(identity.certificate)
networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) }
retrieveNetworkParameters()
// Do all of this in a database transaction so anything that might need a connection has one.
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService)
@ -260,16 +270,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
startShell(rpcOps)
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
}
val networkMapUpdater = NetworkMapUpdater(services.networkMapCache,
NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)),
networkMapClient)
networkMapClient,
networkParameters.serialize().hash)
runOnStop += networkMapUpdater::close
networkMapUpdater.updateNodeInfo(services.myInfo) {
val serialisedNodeInfo = it.serialize()
val signature = services.keyManagementService.sign(serialisedNodeInfo.bytes, it.legalIdentities.first().owningKey)
SignedData(serialisedNodeInfo, signature)
signNodeInfo(it) { publicKey, serialised ->
services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey()
}
}
networkMapUpdater.subscribeToNetworkMap()
@ -658,11 +668,31 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
return PersistentKeyManagementService(identityService, keyPairs)
}
private fun readNetworkParameters() {
val file = configuration.baseDirectory / "network-parameters"
networkParameters = file.readAll().deserialize<SignedData<NetworkParameters>>().verified()
log.info(networkParameters.toString())
check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { "Node is too old for the network" }
private fun retrieveNetworkParameters() {
val networkParamsFile = configuration.baseDirectory.list { paths ->
paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().orElse(null)
}
networkParameters = if (networkParamsFile != null) {
networkParamsFile.readAll().deserialize<SignedData<NetworkParameters>>().verified()
} else {
log.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
val networkMapClient = checkNotNull(networkMapClient) {
"Node hasn't been configured to connect to a network map from which to get the network parameters"
}
val (networkMap, _) = networkMapClient.getNetworkMap()
val signedParams = checkNotNull(networkMapClient.getNetworkParameter(networkMap.networkParameterHash)) {
"Failed loading network parameters from network map server"
}
val verifiedParams = signedParams.verified()
signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
verifiedParams
}
log.info("Loaded network parameters: $networkParameters")
check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion"
}
}
private fun makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService {

View File

@ -144,9 +144,9 @@ open class Node(configuration: NodeConfiguration,
val advertisedAddress = info.addresses.single()
printBasicNodeInfo("Incoming connection address", advertisedAddress.toString())
rpcMessagingClient = RPCMessagingClient(configuration, serverAddress)
rpcMessagingClient = RPCMessagingClient(configuration, serverAddress, networkParameters.maxMessageSize)
verifierMessagingClient = when (configuration.verifierType) {
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics)
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
VerifierType.InMemory -> null
}
return P2PMessagingClient(
@ -156,12 +156,13 @@ open class Node(configuration: NodeConfiguration,
info.legalIdentities[0].owningKey,
serverThread,
database,
advertisedAddress)
advertisedAddress,
networkParameters.maxMessageSize)
}
private fun makeLocalMessageBroker(): NetworkHostAndPort {
with(configuration) {
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager)
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize)
return NetworkHostAndPort("localhost", p2pAddress.port)
}
}

View File

@ -0,0 +1,483 @@
package net.corda.node.internal.protonwrapper.engine
import io.netty.buffer.ByteBuf
import io.netty.buffer.PooledByteBufAllocator
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.debug
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl
import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl
import org.apache.qpid.proton.Proton
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.amqp.messaging.*
import org.apache.qpid.proton.amqp.messaging.Properties
import org.apache.qpid.proton.amqp.messaging.Target
import org.apache.qpid.proton.amqp.transaction.Coordinator
import org.apache.qpid.proton.amqp.transport.ErrorCondition
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode
import org.apache.qpid.proton.amqp.transport.SenderSettleMode
import org.apache.qpid.proton.engine.*
import org.apache.qpid.proton.message.Message
import org.apache.qpid.proton.message.ProtonJMessage
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.util.*
/**
* This ConnectionStateMachine class handles the events generated by the proton-j library to track
* various logical connection, transport and link objects and to drive packet processing.
* It is single threaded per physical SSL connection just like the proton-j library,
* but this threading lock is managed by the EventProcessor class that calls this.
* It ultimately posts application packets to/from from the netty transport pipeline.
*/
internal class ConnectionStateMachine(serverMode: Boolean,
collector: Collector,
private val localLegalName: String,
private val remoteLegalName: String,
userName: String?,
password: String?) : BaseHandler() {
companion object {
private const val IDLE_TIMEOUT = 10000
}
val connection: Connection
private val log = LoggerFactory.getLogger(localLegalName)
private val transport: Transport
private val id = UUID.randomUUID().toString()
private var session: Session? = null
private val messageQueues = mutableMapOf<String, LinkedList<SendableMessageImpl>>()
private val unackedQueue = LinkedList<SendableMessageImpl>()
private val receivers = mutableMapOf<String, Receiver>()
private val senders = mutableMapOf<String, Sender>()
private var tagId: Int = 0
init {
connection = Engine.connection()
connection.container = "CORDA:$id"
transport = Engine.transport()
transport.idleTimeout = IDLE_TIMEOUT
transport.context = connection
transport.setEmitFlowEventOnSend(true)
connection.collect(collector)
val sasl = transport.sasl()
if (userName != null) {
//TODO This handshake is required for our queue permission logic in Artemis
sasl.setMechanisms("PLAIN")
if (serverMode) {
sasl.server()
sasl.done(Sasl.PN_SASL_OK)
} else {
sasl.plain(userName, password)
sasl.client()
}
} else {
sasl.setMechanisms("ANONYMOUS")
if (serverMode) {
sasl.server()
sasl.done(Sasl.PN_SASL_OK)
} else {
sasl.client()
}
}
transport.bind(connection)
if (!serverMode) {
connection.open()
}
}
override fun onConnectionInit(event: Event) {
val connection = event.connection
log.debug { "Connection init $connection" }
}
override fun onConnectionLocalOpen(event: Event) {
val connection = event.connection
log.info("Connection local open $connection")
val session = connection.session()
session.open()
this.session = session
for (target in messageQueues.keys) {
getSender(target)
}
}
override fun onConnectionLocalClose(event: Event) {
val connection = event.connection
log.info("Connection local close $connection")
connection.close()
connection.free()
}
override fun onConnectionUnbound(event: Event) {
if (event.connection == this.connection) {
val channel = connection.context as? Channel
if (channel != null) {
if (channel.isActive) {
channel.close()
}
}
}
}
override fun onConnectionFinal(event: Event) {
val connection = event.connection
log.debug { "Connection final $connection" }
if (connection == this.connection) {
this.connection.context = null
for (queue in messageQueues.values) {
// clear any dead messages
while (true) {
val msg = queue.poll()
if (msg != null) {
msg.doComplete(MessageStatus.Rejected)
msg.release()
} else {
break
}
}
}
messageQueues.clear()
while (true) {
val msg = unackedQueue.poll()
if (msg != null) {
msg.doComplete(MessageStatus.Rejected)
msg.release()
} else {
break
}
}
// shouldn't happen, but close socket channel now if not already done
val channel = connection.context as? Channel
if (channel != null && channel.isActive) {
channel.close()
}
// shouldn't happen, but cleanup any stranded items
transport.context = null
session = null
receivers.clear()
senders.clear()
}
}
override fun onTransportHeadClosed(event: Event) {
val transport = event.transport
log.debug { "Transport Head Closed $transport" }
transport.close_tail()
}
override fun onTransportTailClosed(event: Event) {
val transport = event.transport
log.debug { "Transport Tail Closed $transport" }
transport.close_head()
}
override fun onTransportClosed(event: Event) {
val transport = event.transport
log.debug { "Transport Closed $transport" }
if (transport == this.transport) {
transport.unbind()
transport.free()
transport.context = null
}
}
override fun onTransportError(event: Event) {
val transport = event.transport
log.info("Transport Error $transport")
val condition = event.transport.condition
if (condition != null) {
log.info("Error: ${condition.description}")
} else {
log.info("Error (no description returned).")
}
}
override fun onTransport(event: Event) {
val transport = event.transport
log.debug { "Transport $transport" }
onTransportInternal(transport)
}
private fun onTransportInternal(transport: Transport) {
if (!transport.isClosed) {
val pending = transport.pending() // Note this drives frame generation, which the susbsequent writes push to the socket
if (pending > 0) {
val connection = transport.context as? Connection
val channel = connection?.context as? Channel
channel?.writeAndFlush(transport)
}
}
}
override fun onSessionInit(event: Event) {
val session = event.session
log.debug { "Session init $session" }
}
override fun onSessionLocalOpen(event: Event) {
val session = event.session
log.debug { "Session local open $session" }
}
private fun getSender(target: String): Sender {
if (!senders.containsKey(target)) {
val sender = session!!.sender(UUID.randomUUID().toString())
sender.source = Source().apply {
address = target
dynamic = false
durable = TerminusDurability.NONE
}
sender.target = Target().apply {
address = target
dynamic = false
durable = TerminusDurability.UNSETTLED_STATE
}
sender.senderSettleMode = SenderSettleMode.UNSETTLED
sender.receiverSettleMode = ReceiverSettleMode.FIRST
senders[target] = sender
sender.open()
}
return senders[target]!!
}
override fun onSessionLocalClose(event: Event) {
val session = event.session
log.debug { "Session local close $session" }
session.close()
session.free()
}
override fun onSessionFinal(event: Event) {
val session = event.session
log.debug { "Session final $session" }
if (session == this.session) {
this.session = null
}
}
override fun onLinkLocalOpen(event: Event) {
val link = event.link
if (link is Sender) {
log.debug { "Sender Link local open ${link.name} ${link.source} ${link.target}" }
senders[link.target.address] = link
transmitMessages(link)
}
if (link is Receiver) {
log.debug { "Receiver Link local open ${link.name} ${link.source} ${link.target}" }
receivers[link.target.address] = link
}
}
override fun onLinkRemoteOpen(event: Event) {
val link = event.link
if (link is Receiver) {
if (link.remoteTarget is Coordinator) {
log.debug { "Coordinator link received" }
}
}
}
override fun onLinkFinal(event: Event) {
val link = event.link
if (link is Sender) {
log.debug { "Sender Link final ${link.name} ${link.source} ${link.target}" }
senders.remove(link.target.address)
}
if (link is Receiver) {
log.debug { "Receiver Link final ${link.name} ${link.source} ${link.target}" }
receivers.remove(link.target.address)
}
}
override fun onLinkFlow(event: Event) {
val link = event.link
if (link is Sender) {
log.debug { "Sender Flow event: ${link.name} ${link.source} ${link.target}" }
if (senders.containsKey(link.target.address)) {
transmitMessages(link)
}
} else if (link is Receiver) {
log.debug { "Receiver Flow event: ${link.name} ${link.source} ${link.target}" }
}
}
fun processTransport() {
onTransportInternal(transport)
}
private fun transmitMessages(sender: Sender) {
val messageQueue = messageQueues.getOrPut(sender.target.address, { LinkedList() })
while (sender.credit > 0) {
log.debug { "Sender credit: ${sender.credit}" }
val nextMessage = messageQueue.poll()
if (nextMessage != null) {
try {
val messageBuf = nextMessage.buf!!
val buf = ByteBuffer.allocate(4)
buf.putInt(tagId++)
val delivery = sender.delivery(buf.array())
delivery.context = nextMessage
sender.send(messageBuf.array(), messageBuf.arrayOffset() + messageBuf.readerIndex(), messageBuf.readableBytes())
nextMessage.status = MessageStatus.Sent
log.debug { "Put tag ${javax.xml.bind.DatatypeConverter.printHexBinary(delivery.tag)} on wire uuid: ${nextMessage.applicationProperties["_AMQ_DUPL_ID"]}" }
unackedQueue.offer(nextMessage)
sender.advance()
} finally {
nextMessage.release()
}
} else {
break
}
}
}
override fun onDelivery(event: Event) {
val delivery = event.delivery
log.debug { "Delivery $delivery" }
val link = delivery.link
if (link is Receiver) {
if (delivery.isReadable && !delivery.isPartial) {
val pending = delivery.pending()
val amqpMessage = decodeAMQPMessage(pending, link)
val payload = (amqpMessage.body as Data).value.array
val connection = event.connection
val channel = connection?.context as? Channel
if (channel != null) {
val appProperties = HashMap(amqpMessage.applicationProperties.value)
appProperties["_AMQ_VALIDATED_USER"] = remoteLegalName
val localAddress = channel.localAddress() as InetSocketAddress
val remoteAddress = channel.remoteAddress() as InetSocketAddress
val receivedMessage = ReceivedMessageImpl(
payload,
link.source.address,
remoteLegalName,
NetworkHostAndPort(localAddress.hostString, localAddress.port),
localLegalName,
NetworkHostAndPort(remoteAddress.hostString, remoteAddress.port),
appProperties,
channel,
delivery)
log.debug { "Full message received uuid: ${appProperties["_AMQ_DUPL_ID"]}" }
channel.writeAndFlush(receivedMessage)
if (link.current() == delivery) {
link.advance()
}
} else {
delivery.disposition(Rejected())
delivery.settle()
}
}
} else if (link is Sender) {
log.debug { "Sender delivery confirmed tag ${javax.xml.bind.DatatypeConverter.printHexBinary(delivery.tag)}" }
val ok = delivery.remotelySettled() && delivery.remoteState == Accepted.getInstance()
val sourceMessage = delivery.context as? SendableMessageImpl
unackedQueue.remove(sourceMessage)
sourceMessage?.doComplete(if (ok) MessageStatus.Acknowledged else MessageStatus.Rejected)
delivery.settle()
}
}
private fun encodeAMQPMessage(message: ProtonJMessage): ByteBuf {
val buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(1500)
try {
try {
message.encode(NettyWritable(buffer))
val bytes = ByteArray(buffer.writerIndex())
buffer.readBytes(bytes)
return Unpooled.wrappedBuffer(bytes)
} catch (ex: Exception) {
log.error("Unable to encode message as AMQP packet", ex)
throw ex
}
} finally {
buffer.release()
}
}
private fun encodePayloadBytes(msg: SendableMessageImpl): ByteBuf {
val message = Proton.message() as ProtonJMessage
message.body = Data(Binary(msg.payload))
message.properties = Properties()
val appProperties = HashMap(msg.applicationProperties)
//TODO We shouldn't have to do this, but Artemis Server doesn't set the header on AMQP packets.
// Fortunately, when we are bridge to bridge/bridge to float we can authenticate links there.
appProperties["_AMQ_VALIDATED_USER"] = localLegalName
message.applicationProperties = ApplicationProperties(appProperties)
return encodeAMQPMessage(message)
}
private fun decodeAMQPMessage(pending: Int, link: Receiver): Message {
val msgBuf = PooledByteBufAllocator.DEFAULT.heapBuffer(pending)
try {
link.recv(NettyWritable(msgBuf))
val amqpMessage = Proton.message()
amqpMessage.decode(msgBuf.array(), msgBuf.arrayOffset() + msgBuf.readerIndex(), msgBuf.readableBytes())
return amqpMessage
} finally {
msgBuf.release()
}
}
fun transportWriteMessage(msg: SendableMessageImpl) {
log.debug { "Queue application message write uuid: ${msg.applicationProperties["_AMQ_DUPL_ID"]} ${javax.xml.bind.DatatypeConverter.printHexBinary(msg.payload)}" }
msg.buf = encodePayloadBytes(msg)
val messageQueue = messageQueues.getOrPut(msg.topic, { LinkedList() })
messageQueue.offer(msg)
if (session != null) {
val sender = getSender(msg.topic)
transmitMessages(sender)
}
}
fun transportProcessInput(msg: ByteBuf) {
val source = msg.nioBuffer()
try {
do {
val buffer = transport.inputBuffer
val limit = Math.min(buffer.remaining(), source.remaining())
val duplicate = source.duplicate()
duplicate.limit(source.position() + limit)
buffer.put(duplicate)
transport.processInput().checkIsOk()
source.position(source.position() + limit)
} while (source.hasRemaining())
} catch (ex: Exception) {
val condition = ErrorCondition()
condition.condition = Symbol.getSymbol("proton:io")
condition.description = ex.message
transport.condition = condition
transport.close_tail()
transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code)
}
}
fun transportProcessOutput(ctx: ChannelHandlerContext) {
try {
var done = false
while (!done) {
val toWrite = transport.outputBuffer
if (toWrite != null && toWrite.hasRemaining()) {
val outbound = ctx.alloc().buffer(toWrite.remaining())
outbound.writeBytes(toWrite)
ctx.write(outbound)
transport.outputConsumed()
} else {
done = true
}
}
ctx.flush()
} catch (ex: Exception) {
val condition = ErrorCondition()
condition.condition = Symbol.getSymbol("proton:io")
condition.description = ex.message
transport.condition = condition
transport.close_head()
transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code)
}
}
}

View File

@ -0,0 +1,136 @@
package net.corda.node.internal.protonwrapper.engine
import io.netty.buffer.ByteBuf
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import net.corda.core.utilities.debug
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl
import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl
import org.apache.qpid.proton.Proton
import org.apache.qpid.proton.amqp.messaging.Accepted
import org.apache.qpid.proton.amqp.messaging.Rejected
import org.apache.qpid.proton.amqp.transport.DeliveryState
import org.apache.qpid.proton.amqp.transport.ErrorCondition
import org.apache.qpid.proton.engine.*
import org.apache.qpid.proton.engine.impl.CollectorImpl
import org.apache.qpid.proton.reactor.FlowController
import org.apache.qpid.proton.reactor.Handshaker
import org.slf4j.LoggerFactory
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* The EventProcessor class converts calls on the netty scheduler/pipeline
* into proton-j engine event calls into the ConnectionStateMachine.
* It also registers a couple of standard event processors for the basic connection handshake
* and simple sliding window flow control, so that these events don't have to live inside ConnectionStateMachine.
* Everything here is single threaded, because the proton-j library has to be run that way.
*/
internal class EventProcessor(channel: Channel,
serverMode: Boolean,
localLegalName: String,
remoteLegalName: String,
userName: String?,
password: String?) : BaseHandler() {
companion object {
private const val FLOW_WINDOW_SIZE = 10
}
private val log = LoggerFactory.getLogger(localLegalName)
private val lock = ReentrantLock()
private var pendingExecute: Boolean = false
private val executor: ScheduledExecutorService = channel.eventLoop()
private val collector = Proton.collector() as CollectorImpl
private val handlers = mutableListOf<Handler>()
private val stateMachine: ConnectionStateMachine = ConnectionStateMachine(serverMode,
collector,
localLegalName,
remoteLegalName,
userName,
password)
val connection: Connection = stateMachine.connection
init {
addHandler(Handshaker())
addHandler(FlowController(FLOW_WINDOW_SIZE))
addHandler(stateMachine)
connection.context = channel
tick(stateMachine.connection)
}
fun addHandler(handler: Handler) = handlers.add(handler)
private fun popEvent(): Event? {
var ev = collector.peek()
if (ev != null) {
ev = ev.copy() // prevent mutation by collector.pop()
collector.pop()
}
return ev
}
private fun tick(connection: Connection) {
lock.withLock {
try {
if ((connection.localState != EndpointState.CLOSED) && !connection.transport.isClosed) {
val now = System.currentTimeMillis()
val tickDelay = Math.max(0L, connection.transport.tick(now) - now)
executor.schedule({ tick(connection) }, tickDelay, TimeUnit.MILLISECONDS)
}
} catch (ex: Exception) {
connection.transport.close()
connection.condition = ErrorCondition()
}
}
}
fun processEvents() {
lock.withLock {
pendingExecute = false
log.debug { "Process Events" }
while (true) {
val ev = popEvent() ?: break
log.debug { "Process event: $ev" }
for (handler in handlers) {
handler.handle(ev)
}
}
stateMachine.processTransport()
log.debug { "Process Events Done" }
}
}
fun processEventsAsync() {
lock.withLock {
if (!pendingExecute) {
pendingExecute = true
executor.execute { processEvents() }
}
}
}
fun close() {
if (connection.localState != EndpointState.CLOSED) {
connection.close()
processEvents()
connection.free()
processEvents()
}
}
fun transportProcessInput(msg: ByteBuf) = lock.withLock { stateMachine.transportProcessInput(msg) }
fun transportProcessOutput(ctx: ChannelHandlerContext) = lock.withLock { stateMachine.transportProcessOutput(ctx) }
fun transportWriteMessage(msg: SendableMessageImpl) = lock.withLock { stateMachine.transportWriteMessage(msg) }
fun complete(completer: ReceivedMessageImpl.MessageCompleter) = lock.withLock {
val status: DeliveryState = if (completer.status == MessageStatus.Acknowledged) Accepted.getInstance() else Rejected()
completer.delivery.disposition(status)
completer.delivery.settle()
}
}

View File

@ -0,0 +1,63 @@
package net.corda.node.internal.protonwrapper.engine
import io.netty.buffer.ByteBuf
import org.apache.qpid.proton.codec.WritableBuffer
import java.nio.ByteBuffer
/**
* NettyWritable is a utility class allow proton-j encoders to write directly into a
* netty ByteBuf, without any need to materialize a ByteArray copy.
*/
internal class NettyWritable(val nettyBuffer: ByteBuf) : WritableBuffer {
override fun put(b: Byte) {
nettyBuffer.writeByte(b.toInt())
}
override fun putFloat(f: Float) {
nettyBuffer.writeFloat(f)
}
override fun putDouble(d: Double) {
nettyBuffer.writeDouble(d)
}
override fun put(src: ByteArray, offset: Int, length: Int) {
nettyBuffer.writeBytes(src, offset, length)
}
override fun putShort(s: Short) {
nettyBuffer.writeShort(s.toInt())
}
override fun putInt(i: Int) {
nettyBuffer.writeInt(i)
}
override fun putLong(l: Long) {
nettyBuffer.writeLong(l)
}
override fun hasRemaining(): Boolean {
return nettyBuffer.writerIndex() < nettyBuffer.capacity()
}
override fun remaining(): Int {
return nettyBuffer.capacity() - nettyBuffer.writerIndex()
}
override fun position(): Int {
return nettyBuffer.writerIndex()
}
override fun position(position: Int) {
nettyBuffer.writerIndex(position)
}
override fun put(payload: ByteBuffer) {
nettyBuffer.writeBytes(payload)
}
override fun limit(): Int {
return nettyBuffer.capacity()
}
}

View File

@ -0,0 +1,14 @@
package net.corda.node.internal.protonwrapper.messages
import net.corda.core.utilities.NetworkHostAndPort
/**
* Represents a common interface for both sendable and received application messages.
*/
interface ApplicationMessage {
val payload: ByteArray
val topic: String
val destinationLegalName: String
val destinationLink: NetworkHostAndPort
val applicationProperties: Map<Any?, Any?>
}

View File

@ -0,0 +1,11 @@
package net.corda.node.internal.protonwrapper.messages
/**
* The processing state of a message.
*/
enum class MessageStatus {
Unsent,
Sent,
Acknowledged,
Rejected
}

View File

@ -0,0 +1,13 @@
package net.corda.node.internal.protonwrapper.messages
import net.corda.core.utilities.NetworkHostAndPort
/**
* An extension of ApplicationMessage that includes origin information.
*/
interface ReceivedMessage : ApplicationMessage {
val sourceLegalName: String
val sourceLink: NetworkHostAndPort
fun complete(accepted: Boolean)
}

View File

@ -0,0 +1,10 @@
package net.corda.node.internal.protonwrapper.messages
import net.corda.core.concurrent.CordaFuture
/**
* An extension of ApplicationMessage to allow completion signalling.
*/
interface SendableMessage : ApplicationMessage {
val onComplete: CordaFuture<MessageStatus>
}

View File

@ -0,0 +1,30 @@
package net.corda.node.internal.protonwrapper.messages.impl
import io.netty.channel.Channel
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.messages.ReceivedMessage
import org.apache.qpid.proton.engine.Delivery
/**
* An internal packet management class that allows tracking of asynchronous acknowledgements
* that in turn send Delivery messages back to the originator.
*/
internal class ReceivedMessageImpl(override val payload: ByteArray,
override val topic: String,
override val sourceLegalName: String,
override val sourceLink: NetworkHostAndPort,
override val destinationLegalName: String,
override val destinationLink: NetworkHostAndPort,
override val applicationProperties: Map<Any?, Any?>,
private val channel: Channel,
private val delivery: Delivery) : ReceivedMessage {
data class MessageCompleter(val status: MessageStatus, val delivery: Delivery)
override fun complete(accepted: Boolean) {
val status = if (accepted) MessageStatus.Acknowledged else MessageStatus.Rejected
channel.writeAndFlush(MessageCompleter(status, delivery))
}
override fun toString(): String = "Received ${String(payload)} $topic"
}

View File

@ -0,0 +1,37 @@
package net.corda.node.internal.protonwrapper.messages.impl
import io.netty.buffer.ByteBuf
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.messages.SendableMessage
/**
* An internal packet management class that allows handling of the encoded buffers and
* allows registration of an acknowledgement handler when the remote receiver confirms durable storage.
*/
internal class SendableMessageImpl(override val payload: ByteArray,
override val topic: String,
override val destinationLegalName: String,
override val destinationLink: NetworkHostAndPort,
override val applicationProperties: Map<Any?, Any?>) : SendableMessage {
var buf: ByteBuf? = null
@Volatile
var status: MessageStatus = MessageStatus.Unsent
private val _onComplete = openFuture<MessageStatus>()
override val onComplete: CordaFuture<MessageStatus> get() = _onComplete
fun release() {
buf?.release()
buf = null
}
fun doComplete(status: MessageStatus) {
this.status = status
_onComplete.set(status)
}
override fun toString(): String = "Sendable ${String(payload)} $topic $status"
}

View File

@ -0,0 +1,156 @@
package net.corda.node.internal.protonwrapper.netty
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise
import io.netty.channel.socket.SocketChannel
import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslHandshakeCompletionEvent
import io.netty.util.ReferenceCountUtil
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.toX509CertHolder
import net.corda.core.utilities.debug
import net.corda.node.internal.protonwrapper.engine.EventProcessor
import net.corda.node.internal.protonwrapper.messages.ReceivedMessage
import net.corda.node.internal.protonwrapper.messages.impl.ReceivedMessageImpl
import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl
import org.apache.qpid.proton.engine.ProtonJTransport
import org.apache.qpid.proton.engine.Transport
import org.apache.qpid.proton.engine.impl.ProtocolTracer
import org.apache.qpid.proton.framing.TransportFrame
import org.bouncycastle.cert.X509CertificateHolder
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
/**
* An instance of AMQPChannelHandler sits inside the netty pipeline and controls the socket level lifecycle.
* It also add some extra checks to the SSL handshake to support our non-standard certificate checks of legal identity.
* When a valid SSL connections is made then it initialises a proton-j engine instance to handle the protocol layer.
*/
internal class AMQPChannelHandler(private val serverMode: Boolean,
private val allowedRemoteLegalNames: Set<CordaX500Name>?,
private val userName: String?,
private val password: String?,
private val trace: Boolean,
private val onOpen: (Pair<SocketChannel, ConnectionChange>) -> Unit,
private val onClose: (Pair<SocketChannel, ConnectionChange>) -> Unit,
private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() {
private val log = LoggerFactory.getLogger(allowedRemoteLegalNames?.firstOrNull()?.toString() ?: "AMQPChannelHandler")
private lateinit var remoteAddress: InetSocketAddress
private lateinit var localCert: X509CertificateHolder
private lateinit var remoteCert: X509CertificateHolder
private var eventProcessor: EventProcessor? = null
override fun channelActive(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
remoteAddress = ch.remoteAddress() as InetSocketAddress
val localAddress = ch.localAddress() as InetSocketAddress
log.info("New client connection ${ch.id()} from ${remoteAddress} to ${localAddress}")
}
private fun createAMQPEngine(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
eventProcessor = EventProcessor(ch, serverMode, localCert.subject.toString(), remoteCert.subject.toString(), userName, password)
val connection = eventProcessor!!.connection
val transport = connection.transport as ProtonJTransport
if (trace) {
transport.protocolTracer = object : ProtocolTracer {
override fun sentFrame(transportFrame: TransportFrame) {
log.info("${transportFrame.body}")
}
override fun receivedFrame(transportFrame: TransportFrame) {
log.info("${transportFrame.body}")
}
}
}
ctx.fireChannelActive()
eventProcessor!!.processEventsAsync()
}
override fun channelInactive(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
log.info("Closed client connection ${ch.id()} from ${remoteAddress} to ${ch.localAddress()}")
onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, null, false)))
eventProcessor?.close()
ctx.fireChannelInactive()
}
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is SslHandshakeCompletionEvent) {
if (evt.isSuccess) {
val sslHandler = ctx.pipeline().get(SslHandler::class.java)
localCert = sslHandler.engine().session.localCertificates.first().toX509CertHolder()
remoteCert = sslHandler.engine().session.peerCertificates.first().toX509CertHolder()
try {
val remoteX500Name = CordaX500Name.parse(remoteCert.subject.toString())
require(allowedRemoteLegalNames == null || remoteX500Name in allowedRemoteLegalNames)
log.info("handshake completed subject: ${remoteX500Name}")
} catch (ex: IllegalArgumentException) {
log.error("Invalid certificate subject", ex)
ctx.close()
return
}
createAMQPEngine(ctx)
onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true)))
} else {
log.error("Handshake failure $evt")
ctx.close()
}
}
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
try {
log.debug { "Received $msg" }
if (msg is ByteBuf) {
eventProcessor!!.transportProcessInput(msg)
}
} finally {
ReferenceCountUtil.release(msg)
}
eventProcessor!!.processEventsAsync()
}
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
try {
try {
log.debug { "Sent $msg" }
when (msg) {
// Transfers application packet into the AMQP engine.
is SendableMessageImpl -> {
val inetAddress = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port)
require(inetAddress == remoteAddress) {
"Message for incorrect endpoint"
}
require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.parse(remoteCert.subject.toString())) {
"Message for incorrect legal identity"
}
log.debug { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" }
eventProcessor!!.transportWriteMessage(msg)
}
// A received AMQP packet has been completed and this self-posted packet will be signalled out to the
// external application.
is ReceivedMessage -> {
onReceive(msg)
}
// A general self-posted event that triggers creation of AMQP frames when required.
is Transport -> {
eventProcessor!!.transportProcessOutput(ctx)
}
// A self-posted event that forwards status updates for delivered packets to the application.
is ReceivedMessageImpl.MessageCompleter -> {
eventProcessor!!.complete(msg)
}
}
} catch (ex: Exception) {
log.error("Error in AMQP write processing", ex)
throw ex
}
} finally {
ReferenceCountUtil.release(msg)
}
eventProcessor!!.processEventsAsync()
}
}

View File

@ -0,0 +1,194 @@
package net.corda.node.internal.protonwrapper.netty
import io.netty.bootstrap.Bootstrap
import io.netty.channel.*
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.util.internal.logging.InternalLoggerFactory
import io.netty.util.internal.logging.Slf4JLoggerFactory
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.node.internal.protonwrapper.messages.ReceivedMessage
import net.corda.node.internal.protonwrapper.messages.SendableMessage
import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl
import rx.Observable
import rx.subjects.PublishSubject
import java.security.KeyStore
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.concurrent.withLock
/**
* The AMQPClient creates a connection initiator that will try to connect in a round-robin fashion
* to the first open SSL socket. It will keep retrying until it is stopped.
* To allow thread resource control it can accept a shared thread pool as constructor input,
* otherwise it creates a self-contained Netty thraed pool and socket objects.
* Once connected it can accept application packets to send via the AMQP protocol.
*/
class AMQPClient(val targets: List<NetworkHostAndPort>,
val allowedRemoteLegalNames: Set<CordaX500Name>,
private val userName: String?,
private val password: String?,
private val keyStore: KeyStore,
private val keyStorePrivateKeyPassword: String,
private val trustStore: KeyStore,
private val trace: Boolean = false,
private val sharedThreadPool: EventLoopGroup? = null) : AutoCloseable {
companion object {
init {
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
}
val log = contextLogger()
const val RETRY_INTERVAL = 1000L
const val NUM_CLIENT_THREADS = 2
}
private val lock = ReentrantLock()
@Volatile
private var stopping: Boolean = false
private var workerGroup: EventLoopGroup? = null
@Volatile
private var clientChannel: Channel? = null
// Offset into the list of targets, so that we can implement round-robin reconnect logic.
private var targetIndex = 0
private var currentTarget: NetworkHostAndPort = targets.first()
private val connectListener = object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
if (!future.isSuccess) {
log.info("Failed to connect to $currentTarget")
if (!stopping) {
workerGroup?.schedule({
log.info("Retry connect to $currentTarget")
targetIndex = (targetIndex + 1).rem(targets.size)
restart()
}, RETRY_INTERVAL, TimeUnit.MILLISECONDS)
}
} else {
log.info("Connected to $currentTarget")
// Connection established successfully
clientChannel = future.channel()
clientChannel?.closeFuture()?.addListener(closeListener)
}
}
}
private val closeListener = object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
log.info("Disconnected from $currentTarget")
future.channel()?.disconnect()
clientChannel = null
if (!stopping) {
workerGroup?.schedule({
log.info("Retry connect")
targetIndex = (targetIndex + 1).rem(targets.size)
restart()
}, RETRY_INTERVAL, TimeUnit.MILLISECONDS)
}
}
}
private class ClientChannelInitializer(val parent: AMQPClient) : ChannelInitializer<SocketChannel>() {
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
init {
keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray())
trustManagerFactory.init(parent.trustStore)
}
override fun initChannel(ch: SocketChannel) {
val pipeline = ch.pipeline()
val handler = createClientSslHelper(parent.currentTarget, keyManagerFactory, trustManagerFactory)
pipeline.addLast("sslHandler", handler)
if (parent.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
pipeline.addLast(AMQPChannelHandler(false,
parent.allowedRemoteLegalNames,
parent.userName,
parent.password,
parent.trace,
{ parent._onConnection.onNext(it.second) },
{ parent._onConnection.onNext(it.second) },
{ rcv -> parent._onReceive.onNext(rcv) }))
}
}
fun start() {
lock.withLock {
log.info("connect to: $currentTarget")
workerGroup = sharedThreadPool ?: NioEventLoopGroup(NUM_CLIENT_THREADS)
restart()
}
}
private fun restart() {
val bootstrap = Bootstrap()
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
bootstrap.group(workerGroup).
channel(NioSocketChannel::class.java).
handler(ClientChannelInitializer(this))
currentTarget = targets[targetIndex]
val clientFuture = bootstrap.connect(currentTarget.host, currentTarget.port)
clientFuture.addListener(connectListener)
}
fun stop() {
lock.withLock {
log.info("disconnect from: $currentTarget")
stopping = true
try {
if (sharedThreadPool == null) {
workerGroup?.shutdownGracefully()
workerGroup?.terminationFuture()?.sync()
} else {
clientChannel?.close()?.sync()
}
clientChannel = null
workerGroup = null
} finally {
stopping = false
}
log.info("stopped connection to $currentTarget")
}
}
override fun close() = stop()
val connected: Boolean
get() {
val channel = lock.withLock { clientChannel }
return channel?.isActive ?: false
}
fun createMessage(payload: ByteArray,
topic: String,
destinationLegalName: String,
properties: Map<Any?, Any?>): SendableMessage {
return SendableMessageImpl(payload, topic, destinationLegalName, currentTarget, properties)
}
fun write(msg: SendableMessage) {
val channel = clientChannel
if (channel == null) {
throw IllegalStateException("Connection to $targets not active")
} else {
channel.writeAndFlush(msg)
}
}
private val _onReceive = PublishSubject.create<ReceivedMessage>().toSerialized()
val onReceive: Observable<ReceivedMessage>
get() = _onReceive
private val _onConnection = PublishSubject.create<ConnectionChange>().toSerialized()
val onConnection: Observable<ConnectionChange>
get() = _onConnection
}

View File

@ -0,0 +1,187 @@
package net.corda.node.internal.protonwrapper.netty
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.util.internal.logging.InternalLoggerFactory
import io.netty.util.internal.logging.Slf4JLoggerFactory
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.node.internal.protonwrapper.messages.ReceivedMessage
import net.corda.node.internal.protonwrapper.messages.SendableMessage
import net.corda.node.internal.protonwrapper.messages.impl.SendableMessageImpl
import org.apache.qpid.proton.engine.Delivery
import rx.Observable
import rx.subjects.PublishSubject
import java.net.BindException
import java.net.InetSocketAddress
import java.security.KeyStore
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.concurrent.withLock
/**
* This create a socket acceptor instance that can receive possibly multiple AMQP connections.
* As of now this is not used outside of testing, but in future it will be used for standalone bridging components.
*/
class AMQPServer(val hostName: String,
val port: Int,
private val userName: String?,
private val password: String?,
private val keyStore: KeyStore,
private val keyStorePrivateKeyPassword: String,
private val trustStore: KeyStore,
private val trace: Boolean = false) : AutoCloseable {
companion object {
init {
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
}
private val log = contextLogger()
const val NUM_SERVER_THREADS = 4
}
private val lock = ReentrantLock()
@Volatile
private var stopping: Boolean = false
private var bossGroup: EventLoopGroup? = null
private var workerGroup: EventLoopGroup? = null
private var serverChannel: Channel? = null
private val clientChannels = ConcurrentHashMap<InetSocketAddress, SocketChannel>()
init {
}
private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer<SocketChannel>() {
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
init {
keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray())
trustManagerFactory.init(parent.trustStore)
}
override fun initChannel(ch: SocketChannel) {
val pipeline = ch.pipeline()
val handler = createServerSslHelper(keyManagerFactory, trustManagerFactory)
pipeline.addLast("sslHandler", handler)
if (parent.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
pipeline.addLast(AMQPChannelHandler(true,
null,
parent.userName,
parent.password,
parent.trace,
{
parent.clientChannels.put(it.first.remoteAddress(), it.first)
parent._onConnection.onNext(it.second)
},
{
parent.clientChannels.remove(it.first.remoteAddress())
parent._onConnection.onNext(it.second)
},
{ rcv -> parent._onReceive.onNext(rcv) }))
}
}
fun start() {
lock.withLock {
stop()
bossGroup = NioEventLoopGroup(1)
workerGroup = NioEventLoopGroup(NUM_SERVER_THREADS)
val server = ServerBootstrap()
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
server.group(bossGroup, workerGroup).
channel(NioServerSocketChannel::class.java).
option(ChannelOption.SO_BACKLOG, 100).
handler(LoggingHandler(LogLevel.INFO)).
childHandler(ServerChannelInitializer(this))
log.info("Try to bind $port")
val channelFuture = server.bind(hostName, port).sync() // block/throw here as better to know we failed to claim port than carry on
if (!channelFuture.isDone || !channelFuture.isSuccess) {
throw BindException("Failed to bind port $port")
}
log.info("Listening on port $port")
serverChannel = channelFuture.channel()
}
}
fun stop() {
lock.withLock {
try {
stopping = true
serverChannel?.apply { close() }
serverChannel = null
workerGroup?.shutdownGracefully()
workerGroup?.terminationFuture()?.sync()
bossGroup?.shutdownGracefully()
bossGroup?.terminationFuture()?.sync()
workerGroup = null
bossGroup = null
} finally {
stopping = false
}
}
}
override fun close() = stop()
val listening: Boolean
get() {
val channel = lock.withLock { serverChannel }
return channel?.isActive ?: false
}
fun createMessage(payload: ByteArray,
topic: String,
destinationLegalName: String,
destinationLink: NetworkHostAndPort,
properties: Map<Any?, Any?>): SendableMessage {
val dest = InetSocketAddress(destinationLink.host, destinationLink.port)
require(dest in clientChannels.keys) {
"Destination not available"
}
return SendableMessageImpl(payload, topic, destinationLegalName, destinationLink, properties)
}
fun write(msg: SendableMessage) {
val dest = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port)
val channel = clientChannels[dest]
if (channel == null) {
throw IllegalStateException("Connection to ${msg.destinationLink} not active")
} else {
channel.writeAndFlush(msg)
}
}
fun complete(delivery: Delivery, target: InetSocketAddress) {
val channel = clientChannels[target]
channel?.apply {
writeAndFlush(delivery)
}
}
private val _onReceive = PublishSubject.create<ReceivedMessage>().toSerialized()
val onReceive: Observable<ReceivedMessage>
get() = _onReceive
private val _onConnection = PublishSubject.create<ConnectionChange>().toSerialized()
val onConnection: Observable<ConnectionChange>
get() = _onConnection
}

View File

@ -0,0 +1,6 @@
package net.corda.node.internal.protonwrapper.netty
import org.bouncycastle.cert.X509CertificateHolder
import java.net.InetSocketAddress
data class ConnectionChange(val remoteAddress: InetSocketAddress, val remoteCert: X509CertificateHolder?, val connected: Boolean)

View File

@ -0,0 +1,39 @@
package net.corda.node.internal.protonwrapper.netty
import io.netty.handler.ssl.SslHandler
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport
import java.security.SecureRandom
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
internal fun createClientSslHelper(target: NetworkHostAndPort,
keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS")
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers
sslContext.init(keyManagers, trustManagers, SecureRandom())
val sslEngine = sslContext.createSSLEngine(target.host, target.port)
sslEngine.useClientMode = true
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
sslEngine.enableSessionCreation = true
return SslHandler(sslEngine)
}
internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS")
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers
sslContext.init(keyManagers, trustManagers, SecureRandom())
val sslEngine = sslContext.createSSLEngine()
sslEngine.useClientMode = false
sslEngine.needClientAuth = true
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
sslEngine.enableSessionCreation = true
return SslHandler(sslEngine)
}

View File

@ -34,7 +34,7 @@ fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): A
password.use {
return try {
authenticate(principal, password)
} catch (e: AuthenticationException) {
} catch (e: FailedLoginException) {
null
}
}

View File

@ -95,8 +95,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
// Setup optional cache layer if configured
it.cacheManager = config.options?.cache?.let {
GuavaCacheManager(
timeToLiveSeconds = it.expiryTimeInSecs,
maxSize = it.capacity)
timeToLiveSeconds = it.expireAfterSecs,
maxSize = it.maxEntries)
}
}
}
@ -149,22 +149,29 @@ private object RPCPermissionResolver : PermissionResolver {
private val ACTION_START_FLOW = "startflow"
private val ACTION_INVOKE_RPC = "invokerpc"
private val ACTION_ALL = "all"
private val FLOW_RPC_CALLS = setOf("startFlowDynamic", "startTrackedFlowDynamic")
private val FLOW_RPC_CALLS = setOf(
"startFlowDynamic",
"startTrackedFlowDynamic",
"startFlow",
"startTrackedFlow")
override fun resolvePermission(representation: String): Permission {
val action = representation.substringBefore(SEPARATOR).toLowerCase()
val action = representation.substringBefore(SEPARATOR).toLowerCase()
when (action) {
ACTION_INVOKE_RPC -> {
val rpcCall = representation.substringAfter(SEPARATOR)
require(representation.count { it == SEPARATOR } == 1) {
val rpcCall = representation.substringAfter(SEPARATOR, "")
require(representation.count { it == SEPARATOR } == 1 && !rpcCall.isEmpty()) {
"Malformed permission string"
}
return RPCPermission(setOf(rpcCall))
val permitted = when(rpcCall) {
"startFlow" -> setOf("startFlowDynamic", rpcCall)
"startTrackedFlow" -> setOf("startTrackedFlowDynamic", rpcCall)
else -> setOf(rpcCall)
}
return RPCPermission(permitted)
}
ACTION_START_FLOW -> {
val targetFlow = representation.substringAfter(SEPARATOR)
val targetFlow = representation.substringAfter(SEPARATOR, "")
require(targetFlow.isNotEmpty()) {
"Missing target flow after StartFlow"
}

View File

@ -6,10 +6,10 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
import net.corda.node.services.messaging.CertificateChainCheckPolicy
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import java.net.URL
import java.nio.file.Path
import java.util.*
@ -44,6 +44,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
val sshd: SSHDConfiguration?
val database: DatabaseConfig
val relay: RelayConfiguration?
val useAMQPBridges: Boolean get() = true
}
data class DevModeOptions(val disableCheckpointChecker: Boolean = false)
@ -120,7 +121,8 @@ data class NodeConfigurationImpl(
// TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(),
override val sshd: SSHDConfiguration? = null,
override val database: DatabaseConfig = DatabaseConfig(exportHibernateJMXStatistics = devMode)
override val database: DatabaseConfig = DatabaseConfig(exportHibernateJMXStatistics = devMode),
override val useAMQPBridges: Boolean = true
) : NodeConfiguration {
override val exportJMXto: String get() = "http"
@ -200,7 +202,7 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
data class Options(val cache: Options.Cache?) {
// Cache parameters
data class Cache(val expiryTimeInSecs: Long, val capacity: Long)
data class Cache(val expireAfterSecs: Long, val maxEntries: Long)
}

View File

@ -24,7 +24,7 @@ import javax.annotation.concurrent.ThreadSafe
* @param identities initial set of identities for the service, typically only used for unit tests.
*/
@ThreadSafe
class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
class InMemoryIdentityService(identities: Array<out PartyAndCertificate>,
trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal {
companion object {
private val log = contextLogger()

View File

@ -0,0 +1,203 @@
package net.corda.node.services.messaging
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.debug
import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.netty.AMQPClient
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.crypto.loadKeyStore
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.slf4j.LoggerFactory
import rx.Subscription
import java.security.KeyStore
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* The AMQPBridgeManager holds the list of independent AMQPBridge objects that actively ferry messages to remote Artemis
* inboxes.
* The AMQPBridgeManager also provides a single shared connection to Artemis, although each bridge then creates an
* independent Session for message consumption.
* The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager.
*/
internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress: NetworkHostAndPort, val maxMessageSize: Int) : BridgeManager {
private val lock = ReentrantLock()
private val bridgeNameToBridgeMap = mutableMapOf<String, AMQPBridge>()
private var sharedEventLoopGroup: EventLoopGroup? = null
private val keyStore = loadKeyStore(config.sslKeystore, config.keyStorePassword)
private val keyStorePrivateKeyPassword: String = config.keyStorePassword
private val trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword)
private var artemis: ArtemisMessagingClient? = null
companion object {
private const val NUM_BRIDGE_THREADS = 0 // Default sized pool
}
/**
* Each AMQPBridge is an independent consumer of messages from the Artemis local queue per designated endpoint.
* It attempts to deliver these messages via an AMQPClient instance to the remote Artemis inbox.
* To prevent race conditions the Artemis session/consumer is only created when the AMQPClient has a stable AMQP connection.
* The acknowledgement and removal of messages from the local queue only occurs if there successful end-to-end delivery.
* If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery,
* however Artemis and the remote Corda instanced will deduplicate these messages.
*/
private class AMQPBridge(private val queueName: String,
private val target: NetworkHostAndPort,
private val legalNames: Set<CordaX500Name>,
keyStore: KeyStore,
keyStorePrivateKeyPassword: String,
trustStore: KeyStore,
sharedEventGroup: EventLoopGroup,
private val artemis: ArtemisMessagingClient) {
companion object {
fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
}
private val log = LoggerFactory.getLogger("$bridgeName:${legalNames.first()}")
val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, sharedThreadPool = sharedEventGroup)
val bridgeName: String get() = getBridgeName(queueName, target)
private val lock = ReentrantLock() // lock to serialise session level access
private var session: ClientSession? = null
private var consumer: ClientConsumer? = null
private var connectedSubscription: Subscription? = null
fun start() {
log.info("Create new AMQP bridge")
connectedSubscription = amqpClient.onConnection.subscribe({ x -> onSocketConnected(x.connected) })
amqpClient.start()
}
fun stop() {
log.info("Stopping AMQP bridge")
lock.withLock {
synchronized(artemis) {
consumer?.close()
consumer = null
session?.stop()
session = null
}
}
amqpClient.stop()
connectedSubscription?.unsubscribe()
connectedSubscription = null
}
private fun onSocketConnected(connected: Boolean) {
lock.withLock {
synchronized(artemis) {
if (connected) {
log.info("Bridge Connected")
val sessionFactory = artemis.started!!.sessionFactory
val session = sessionFactory.createSession(NODE_USER, NODE_USER, false, false, false, false, DEFAULT_ACK_BATCH_SIZE)
this.session = session
val consumer = session.createConsumer(queueName)
this.consumer = consumer
consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler)
session.start()
} else {
log.info("Bridge Disconnected")
consumer?.close()
consumer = null
session?.stop()
session = null
}
}
}
}
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
lock.withLock {
val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) }
val properties = HashMap<Any?, Any?>()
for (key in artemisMessage.propertyNames) {
var value = artemisMessage.getObjectProperty(key)
if (value is SimpleString) {
value = value.toString()
}
properties[key.toString()] = value
}
log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" }
val sendableMessage = amqpClient.createMessage(data, P2P_QUEUE,
legalNames.first().toString(),
properties)
sendableMessage.onComplete.then {
log.debug { "Bridge ACK ${sendableMessage.onComplete.get()}" }
lock.withLock {
if (sendableMessage.onComplete.get() == MessageStatus.Acknowledged) {
artemisMessage.acknowledge()
session?.commit()
} else {
log.info("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
session?.rollback(false)
}
}
}
amqpClient.write(sendableMessage)
}
}
}
private fun gatherAddresses(node: NodeInfo): Sequence<ArtemisMessagingComponent.ArtemisPeerAddress> {
val address = node.addresses.first()
return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence()
}
override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) {
if (bridgeExists(getBridgeName(queueName, target))) {
return
}
val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, sharedEventLoopGroup!!, artemis!!)
lock.withLock {
bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge
}
newBridge.start()
}
override fun destroyBridges(node: NodeInfo) {
lock.withLock {
gatherAddresses(node).forEach {
val bridge = bridgeNameToBridgeMap.remove(getBridgeName(it.queueName, it.hostAndPort))
bridge?.stop()
}
}
}
override fun bridgeExists(bridgeName: String): Boolean = lock.withLock { bridgeNameToBridgeMap.containsKey(bridgeName) }
override fun start() {
sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS)
val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize)
this.artemis = artemis
artemis.start()
}
override fun stop() = close()
override fun close() {
lock.withLock {
for (bridge in bridgeNameToBridgeMap.values) {
bridge.stop()
}
sharedEventLoopGroup?.shutdownGracefully()
sharedEventLoopGroup?.terminationFuture()?.sync()
sharedEventLoopGroup = null
bridgeNameToBridgeMap.clear()
artemis?.stop()
}
}
}

View File

@ -3,14 +3,17 @@ package net.corda.node.services.messaging
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.client.*
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientProducer
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort) {
class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) {
companion object {
private val log = loggerFor<ArtemisMessagingClient>()
}
@ -30,7 +33,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s
// would be the default and the two lines below can be deleted.
connectionTTL = -1
clientFailureCheckPeriod = -1
minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE
minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null
}
val sessionFactory = locator.createSessionFactory()
@ -46,7 +49,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s
}
fun stop() = synchronized(this) {
started!!.run {
started?.run {
producer.close()
// Ensure any trailing messages are committed to the journal
session.commit()

View File

@ -1,6 +1,5 @@
package net.corda.node.services.messaging
import io.netty.handler.ssl.SslHandler
import net.corda.core.crypto.AddressFormatException
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
@ -24,11 +23,10 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.nodeapi.*
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
@ -37,15 +35,17 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.config.Configuration
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.*
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin
@ -53,22 +53,17 @@ import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.core.settings.HierarchicalRepository
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
import org.apache.activemq.artemis.core.settings.impl.AddressSettings
import org.apache.activemq.artemis.spi.core.remoting.*
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import org.apache.activemq.artemis.utils.ConfigurationHelper
import rx.Subscription
import java.io.IOException
import java.math.BigInteger
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.Principal
import java.time.Duration
import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.ScheduledExecutorService
import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler
@ -80,7 +75,6 @@ import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.RE
import javax.security.auth.login.FailedLoginException
import javax.security.auth.login.LoginException
import javax.security.auth.spi.LoginModule
import javax.security.auth.x500.X500Principal
import javax.security.cert.CertificateException
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
@ -101,14 +95,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private val p2pPort: Int,
val rpcPort: Int?,
val networkMapCache: NetworkMapCache,
val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() {
val securityManager: RPCSecurityManager,
val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object {
private val log = contextLogger()
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
@JvmStatic
val MAX_FILE_SIZE = 10485760
}
private class InnerState {
var running = false
}
@ -117,6 +108,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private lateinit var activeMQServer: ActiveMQServer
val serverControl: ActiveMQServerControl get() = activeMQServer.activeMQServerControl
private var networkChangeHandle: Subscription? = null
private lateinit var bridgeManager: BridgeManager
init {
config.baseDirectory.requireOnDefaultFileSystem()
@ -136,6 +128,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
}
fun stop() = mutex.locked {
bridgeManager.close()
networkChangeHandle?.unsubscribe()
networkChangeHandle = null
activeMQServer.stop()
@ -156,7 +149,14 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
registerPostQueueCreationCallback { deployBridgesFromNewQueue(it.toString()) }
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
}
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
bridgeManager = if (config.useAMQPBridges) {
AMQPBridgeManager(config, NetworkHostAndPort("localhost", p2pPort), maxMessageSize)
} else {
CoreBridgeManager(config, activeMQServer)
}
activeMQServer.start()
bridgeManager.start()
Node.printBasicNodeInfo("Listening on port", p2pPort.toString())
if (rpcPort != null) {
Node.printBasicNodeInfo("RPC service listening on port", rpcPort.toString())
@ -181,9 +181,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
isPersistIDCache = true
isPopulateValidatedUser = true
journalBufferSize_NIO = MAX_FILE_SIZE // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store.
journalBufferSize_AIO = MAX_FILE_SIZE // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
journalFileSize = MAX_FILE_SIZE // The size of each journal file in bytes. Artemis default is 10MiB.
journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store.
journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB.
managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS)
// Artemis allows multiple servers to be grouped together into a cluster for load balancing purposes. The cluster
// user is used for connecting the nodes together. It has super-user privileges and so it's imperative that its
@ -211,15 +211,17 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
)
addressesSettings = mapOf(
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply {
maxSizeBytes = 10L * MAX_FILE_SIZE
maxSizeBytes = 10L * maxMessageSize
addressFullMessagePolicy = AddressFullMessagePolicy.FAIL
}
)
// JMX enablement
if (config.exportJMXto.isNotEmpty()) {isJMXManagementEnabled = true
isJMXUseBrokerName = true}
// JMX enablement
if (config.exportJMXto.isNotEmpty()) {
isJMXManagementEnabled = true
isJMXUseBrokerName = true
}
}.configureAddressSecurity()
}.configureAddressSecurity()
private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
@ -302,7 +304,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
fun deployBridgeToPeer(nodeInfo: NodeInfo) {
log.debug("Deploying bridge for $queueName to $nodeInfo")
val address = nodeInfo.addresses.first()
deployBridge(queueName, address, nodeInfo.legalIdentitiesAndCerts.map { it.name }.toSet())
bridgeManager.deployBridge(queueName, address, nodeInfo.legalIdentitiesAndCerts.map { it.name }.toSet())
}
if (queueName.startsWith(PEERS_PREFIX)) {
@ -337,147 +339,39 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
fun deployBridges(node: NodeInfo) {
gatherAddresses(node)
.filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) }
.filter { queueExists(it.queueName) && !bridgeManager.bridgeExists(it.bridgeName) }
.forEach { deployBridge(it, node.legalIdentitiesAndCerts.map { it.name }.toSet()) }
}
fun destroyBridges(node: NodeInfo) {
gatherAddresses(node).forEach {
activeMQServer.destroyBridge(it.bridgeName)
}
}
when (change) {
is MapChange.Added -> {
deployBridges(change.node)
}
is MapChange.Removed -> {
destroyBridges(change.node)
bridgeManager.destroyBridges(change.node)
}
is MapChange.Modified -> {
// TODO Figure out what has actually changed and only destroy those bridges that need to be.
destroyBridges(change.previousNode)
bridgeManager.destroyBridges(change.previousNode)
deployBridges(change.node)
}
}
}
private fun deployBridge(address: ArtemisPeerAddress, legalNames: Set<CordaX500Name>) {
deployBridge(address.queueName, address.hostAndPort, legalNames)
bridgeManager.deployBridge(address.queueName, address.hostAndPort, legalNames)
}
private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) =
ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL)
/**
* All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
* messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it,
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
* P2P address.
*/
private fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) {
val connectionDirection = ConnectionDirection.Outbound(
connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name,
expectedCommonNames = legalNames
)
val tcpTransport = createTcpTransport(connectionDirection, target.host, target.port)
tcpTransport.params[ArtemisMessagingServer::class.java.name] = this
// We intentionally overwrite any previous connector config in case the peer legal name changed
activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport)
activeMQServer.deployBridge(BridgeConfiguration().apply {
name = getBridgeName(queueName, target)
this.queueName = queueName
forwardingAddress = P2P_QUEUE
staticConnectors = listOf(target.toString())
confirmationWindowSize = 100000 // a guess
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
// We keep trying until the network map deems the node unreachable and tells us it's been removed at which
// point we destroy the bridge
retryInterval = config.activeMQServer.bridge.retryIntervalMs
retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier
maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis()
// As a peer of the target node we must connect to it using the peer user. Actual authentication is done using
// our TLS certificate.
user = PEER_USER
password = PEER_USER
})
}
private fun queueExists(queueName: String): Boolean = activeMQServer.queueQuery(SimpleString(queueName)).isExists
private fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName)
private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort)
private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
}
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
override fun createConnector(configuration: MutableMap<String, Any>,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?): Connector {
return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool,
protocolManager)
}
}
private class VerifyingNettyConnector(configuration: MutableMap<String, Any>,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?) :
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) {
companion object {
private val log = contextLogger()
}
private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration)
override fun createConnection(): Connection? {
val connection = super.createConnection() as? NettyConnection
if (sslEnabled && connection != null) {
val expectedLegalNames: Set<CordaX500Name> = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet<CordaX500Name>())
try {
val session = connection.channel
.pipeline()
.get(SslHandler::class.java)
.engine()
.session
// Checks the peer name is the one we are expecting.
// TODO Some problems here: after introduction of multiple legal identities on the node and removal of the main one,
// we run into the issue, who are we connecting to. There are some solutions to that: advertise `network identity`;
// have mapping port -> identity (but, design doc says about removing SingleMessageRecipient and having just NetworkHostAndPort,
// it was convenient to store that this way); SNI.
val peerLegalName = CordaX500Name.parse(session.peerPrincipal.name)
val expectedLegalName = expectedLegalNames.singleOrNull { it == peerLegalName }
require(expectedLegalName != null) {
"Peer has wrong CN - expected $expectedLegalNames but got $peerLegalName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!"
}
// Make sure certificate has the same name.
val peerCertificateName = CordaX500Name.build(X500Principal(session.peerCertificateChain[0].subjectDN.name))
require(peerCertificateName == expectedLegalName) {
"Peer has wrong subject name in the certificate - expected $expectedLegalNames but got $peerCertificateName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!"
}
X509Utilities.validateCertificateChain(session.localCertificates.last() as java.security.cert.X509Certificate, *session.peerCertificates)
} catch (e: IllegalArgumentException) {
connection.close()
log.error(e.message)
return null
}
}
return connection
}
}
sealed class CertificateChainCheckPolicy {
@FunctionalInterface

View File

@ -0,0 +1,20 @@
package net.corda.node.services.messaging
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
/**
* Provides an internal interface that the [ArtemisMessagingServer] delegates to for Bridge activities.
*/
internal interface BridgeManager : AutoCloseable {
fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>)
fun destroyBridges(node: NodeInfo)
fun bridgeExists(bridgeName: String): Boolean
fun start()
fun stop()
}

View File

@ -0,0 +1,166 @@
package net.corda.node.services.messaging
import io.netty.handler.ssl.SslHandler
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.spi.core.remoting.*
import org.apache.activemq.artemis.utils.ConfigurationHelper
import java.time.Duration
import java.util.concurrent.Executor
import java.util.concurrent.ScheduledExecutorService
import javax.security.auth.x500.X500Principal
/**
* This class simply moves the legacy CORE bridge code from [ArtemisMessagingServer]
* into a class implementing [BridgeManager].
* It has no lifecycle events, because the bridges are internal to the ActiveMQServer instance and thus
* stop when it is stopped.
*/
internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServer: ActiveMQServer) : BridgeManager {
companion object {
private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
private val ArtemisMessagingComponent.ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort)
}
private fun gatherAddresses(node: NodeInfo): Sequence<ArtemisMessagingComponent.ArtemisPeerAddress> {
val address = node.addresses.first()
return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence()
}
/**
* All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
* messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it,
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
* P2P address.
*/
override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) {
val connectionDirection = ConnectionDirection.Outbound(
connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name,
expectedCommonNames = legalNames
)
val tcpTransport = ArtemisTcpTransport.tcpTransport(connectionDirection, target, config, enableSSL = true)
tcpTransport.params[ArtemisMessagingServer::class.java.name] = this
// We intentionally overwrite any previous connector config in case the peer legal name changed
activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport)
activeMQServer.deployBridge(BridgeConfiguration().apply {
name = getBridgeName(queueName, target)
this.queueName = queueName
forwardingAddress = P2P_QUEUE
staticConnectors = listOf(target.toString())
confirmationWindowSize = 100000 // a guess
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
// We keep trying until the network map deems the node unreachable and tells us it's been removed at which
// point we destroy the bridge
retryInterval = config.activeMQServer.bridge.retryIntervalMs
retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier
maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis()
// As a peer of the target node we must connect to it using the peer user. Actual authentication is done using
// our TLS certificate.
user = PEER_USER
password = PEER_USER
})
}
override fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName)
override fun start() {
// Nothing to do
}
override fun stop() {
// Nothing to do
}
override fun close() = stop()
override fun destroyBridges(node: NodeInfo) {
gatherAddresses(node).forEach {
activeMQServer.destroyBridge(it.bridgeName)
}
}
}
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
override fun createConnector(configuration: MutableMap<String, Any>,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?): Connector {
return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool,
protocolManager)
}
private class VerifyingNettyConnector(configuration: MutableMap<String, Any>,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?) :
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) {
companion object {
private val log = contextLogger()
}
private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration)
override fun createConnection(): Connection? {
val connection = super.createConnection() as? NettyConnection
if (sslEnabled && connection != null) {
val expectedLegalNames: Set<CordaX500Name> = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet<CordaX500Name>())
try {
val session = connection.channel
.pipeline()
.get(SslHandler::class.java)
.engine()
.session
// Checks the peer name is the one we are expecting.
// TODO Some problems here: after introduction of multiple legal identities on the node and removal of the main one,
// we run into the issue, who are we connecting to. There are some solutions to that: advertise `network identity`;
// have mapping port -> identity (but, design doc says about removing SingleMessageRecipient and having just NetworkHostAndPort,
// it was convenient to store that this way); SNI.
val peerLegalName = CordaX500Name.parse(session.peerPrincipal.name)
val expectedLegalName = expectedLegalNames.singleOrNull { it == peerLegalName }
require(expectedLegalName != null) {
"Peer has wrong CN - expected $expectedLegalNames but got $peerLegalName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!"
}
// Make sure certificate has the same name.
val peerCertificateName = CordaX500Name.build(X500Principal(session.peerCertificateChain[0].subjectDN.name))
require(peerCertificateName == expectedLegalName) {
"Peer has wrong subject name in the certificate - expected $expectedLegalNames but got $peerCertificateName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!"
}
X509Utilities.validateCertificateChain(session.localCertificates.last() as java.security.cert.X509Certificate, *session.peerCertificates)
} catch (e: IllegalArgumentException) {
connection.close()
log.error(e.message)
return null
}
}
return connection
}
}
}

View File

@ -67,7 +67,8 @@ class P2PMessagingClient(config: NodeConfiguration,
private val myIdentity: PublicKey,
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
private val database: CordaPersistence,
advertisedAddress: NetworkHostAndPort = serverAddress
advertisedAddress: NetworkHostAndPort = serverAddress,
private val maxMessageSize: Int
) : SingletonSerializeAsToken(), MessagingService {
companion object {
private val log = contextLogger()
@ -143,7 +144,7 @@ class P2PMessagingClient(config: NodeConfiguration,
override val myAddress: SingleMessageRecipient = NodeAddress(myIdentity, advertisedAddress)
private val messageRedeliveryDelaySeconds = config.messageRedeliveryDelaySeconds.toLong()
private val artemis = ArtemisMessagingClient(config, serverAddress)
private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize)
private val state = ThreadBox(InnerState())
private val handlers = ConcurrentHashMap<String, MessageHandler>()

View File

@ -12,8 +12,8 @@ import net.corda.nodeapi.internal.crypto.getX509Certificate
import net.corda.nodeapi.internal.crypto.loadKeyStore
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort) : SingletonSerializeAsToken() {
private val artemis = ArtemisMessagingClient(config, serverAddress)
class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize)
private var rpcServer: RPCServer? = null
fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {

View File

@ -17,13 +17,13 @@ import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.*
import java.util.concurrent.*
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry) : SingletonSerializeAsToken() {
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object {
private val log = loggerFor<VerifierMessagingClient>()
private val verifierResponseAddress = "$VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX.${random63BitValue()}"
}
private val artemis = ArtemisMessagingClient(config, serverAddress)
private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize)
/** An executor for sending messages */
private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("Messaging", 1)
private var verificationResponseConsumer: ClientConsumer? = null

View File

@ -12,10 +12,10 @@ import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory
import net.corda.nodeapi.internal.NetworkMap
import net.corda.nodeapi.internal.NetworkParameters
import net.corda.nodeapi.internal.SignedNetworkMap
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.SignedNetworkMap
import net.corda.nodeapi.internal.SignedNodeInfo
import okhttp3.CacheControl
import okhttp3.Headers
import rx.Subscription
@ -31,13 +31,13 @@ import java.util.concurrent.TimeUnit
class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) {
private val networkMapUrl = URL("$compatibilityZoneURL/network-map")
fun publish(signedNodeInfo: SignedData<NodeInfo>) {
fun publish(signedNodeInfo: SignedNodeInfo) {
val publishURL = URL("$networkMapUrl/publish")
val conn = publishURL.openHttpConnection()
conn.doOutput = true
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/octet-stream")
conn.outputStream.use { it.write(signedNodeInfo.serialize().bytes) }
conn.outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
// This will throw IOException if the response code is not HTTP 200.
// This gives a much better exception then reading the error stream.
@ -47,9 +47,7 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
fun getNetworkMap(): NetworkMapResponse {
val conn = networkMapUrl.openHttpConnection()
val signedNetworkMap = conn.inputStream.use { it.readBytes() }.deserialize<SignedNetworkMap>()
val networkMap = signedNetworkMap.verified()
// Assume network map cert is issued by the root.
X509Utilities.validateCertificateChain(trustedRoot, signedNetworkMap.sig.by, trustedRoot)
val networkMap = signedNetworkMap.verified(trustedRoot)
val timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds
return NetworkMapResponse(networkMap, timeout)
}
@ -59,16 +57,12 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize<SignedData<NodeInfo>>()
val nodeInfo = signedNodeInfo.verified()
// Verify node info is signed by node identity
// TODO : Validate multiple signatures when NodeInfo supports multiple identities.
require(nodeInfo.legalIdentities.any { it.owningKey == signedNodeInfo.sig.by }) { "NodeInfo must be signed by the node owning key." }
nodeInfo
val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize<SignedNodeInfo>()
signedNodeInfo.verified()
}
}
fun getNetworkParameter(networkParameterHash: SecureHash): NetworkParameters? {
fun getNetworkParameter(networkParameterHash: SecureHash): SignedData<NetworkParameters>? {
val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection()
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
@ -87,7 +81,8 @@ data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Durat
class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val fileWatcher: NodeInfoWatcher,
private val networkMapClient: NetworkMapClient?) : Closeable {
private val networkMapClient: NetworkMapClient?,
private val currentParametersHash: SecureHash) : Closeable {
companion object {
private val logger = contextLogger()
private val retryInterval = 1.minutes
@ -101,7 +96,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS)
}
fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedData<NodeInfo>) {
fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedNodeInfo) {
val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first())
// Compare node info without timestamp.
if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return
@ -127,15 +122,21 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
override fun run() {
val nextScheduleDelay = try {
val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap()
// TODO NetworkParameters updates are not implemented yet. Every mismatch should result in node shutdown.
if (currentParametersHash != networkMap.networkParameterHash) {
logger.error("Node is using parameters with hash: $currentParametersHash but network map is advertising: ${networkMap.networkParameterHash}.\n" +
"Please update node to use correct network parameters file.\"")
System.exit(1)
}
val currentNodeHashes = networkMapCache.allNodeHashes
val hashesFromNetworkMap = networkMap.nodeInfoHashes
(hashesFromNetworkMap - currentNodeHashes).mapNotNull {
// Download new node info from network map
try {
networkMapClient.getNodeInfo(it)
} catch (t: Throwable) {
} catch (e: Exception) {
// Failure to retrieve one node info shouldn't stop the whole update, log and return null instead.
logger.warn("Error encountered when downloading node info '$it', skipping...", t)
logger.warn("Error encountered when downloading node info '$it', skipping...", e)
null
}
}.forEach {
@ -146,7 +147,6 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
(currentNodeHashes - hashesFromNetworkMap - fileWatcher.processedNodeInfoHashes)
.mapNotNull(networkMapCache::getNodeByHash)
.forEach(networkMapCache::removeNode)
// TODO: Check NetworkParameter.
cacheTimeout
} catch (t: Throwable) {
logger.warn("Error encountered while updating network map, will retry in ${retryInterval.seconds} seconds", t)
@ -159,7 +159,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
executor.submit(task) // The check may be expensive, so always run it in the background even the first time.
}
private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedData<NodeInfo>, networkMapClient: NetworkMapClient) {
private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) {
val task = object : Runnable {
override fun run() {
try {

View File

@ -2,14 +2,14 @@ package net.corda.node.services.network
import net.corda.cordform.CordformNode
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.internal.*
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.NodeInfoFilesCopier
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.nodeapi.internal.SignedNodeInfo
import rx.Observable
import rx.Scheduler
import java.io.IOException
@ -41,14 +41,14 @@ class NodeInfoWatcher(private val nodePath: Path,
private val logger = contextLogger()
/**
* Saves the given [NodeInfo] to a path.
* The node is 'encoded' as a SignedData<NodeInfo>, signed with the owning key of its first identity.
* The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity.
* The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename
* is used so that one can freely copy these files without fearing to overwrite another one.
*
* @param path the path where to write the file, if non-existent it will be created.
* @param signedNodeInfo the signed NodeInfo.
*/
fun saveToFile(path: Path, signedNodeInfo: SignedData<NodeInfo>) {
fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) {
try {
path.createDirectories()
signedNodeInfo.serialize()
@ -85,7 +85,7 @@ class NodeInfoWatcher(private val nodePath: Path,
.flatMapIterable { loadFromDirectory() }
}
fun saveToFile(signedNodeInfo: SignedData<NodeInfo>) = Companion.saveToFile(nodePath, signedNodeInfo)
fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo)
/**
* Loads all the files contained in a given path and returns the deserialized [NodeInfo]s.
@ -118,7 +118,7 @@ class NodeInfoWatcher(private val nodePath: Path,
private fun processFile(file: Path): NodeInfo? {
return try {
logger.info("Reading NodeInfo from file: $file")
val signedData = file.readAll().deserialize<SignedData<NodeInfo>>()
val signedData = file.readAll().deserialize<SignedNodeInfo>()
signedData.verified()
} catch (e: Exception) {
logger.warn("Exception parsing NodeInfo from file. $file", e)

View File

@ -25,7 +25,7 @@ import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
import net.corda.nodeapi.internal.NotaryInfo
import net.corda.nodeapi.internal.network.NotaryInfo
import org.hibernate.Session
import rx.Observable
import rx.subjects.PublishSubject
@ -208,9 +208,6 @@ open class PersistentNetworkMapCache(
getAllInfos(session).map { it.toNodeInfo() }
}
// Changes related to NetworkMap redesign
// TODO It will be properly merged into network map cache after services removal.
private fun getAllInfos(session: Session): List<NodeInfoSchemaV1.PersistentNodeInfo> {
val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java)
criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java))
@ -293,7 +290,6 @@ open class PersistentNetworkMapCache(
else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port")
}
/** Object Relational Mapping support. */
private fun generateMappedObject(nodeInfo: NodeInfo): NodeInfoSchemaV1.PersistentNodeInfo {
return NodeInfoSchemaV1.PersistentNodeInfo(

View File

@ -33,3 +33,5 @@ enterpriseConfiguration = {
waitInterval = 40000
}
}
useAMQPBridges = true

View File

@ -24,10 +24,9 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence;
import net.corda.nodeapi.internal.persistence.DatabaseTransaction;
import net.corda.testing.SerializationEnvironmentRule;
import net.corda.testing.TestIdentity;
import net.corda.testing.contracts.DummyLinearContract;
import net.corda.testing.contracts.VaultFiller;
import net.corda.testing.internal.vault.DummyLinearContract;
import net.corda.testing.internal.vault.VaultFiller;
import net.corda.testing.node.MockServices;
import net.corda.testing.schemas.DummyLinearStateSchemaV1;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@ -46,20 +45,20 @@ import java.util.stream.StreamSupport;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.CoreTestUtils.rigorousMock;
import static net.corda.testing.TestConstants.getBOC_NAME;
import static net.corda.testing.TestConstants.getCHARLIE_NAME;
import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME;
import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock;
import static net.corda.testing.TestConstants.BOC_NAME;
import static net.corda.testing.TestConstants.CHARLIE_NAME;
import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME;
import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices;
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
import static org.assertj.core.api.Assertions.assertThat;
public class VaultQueryJavaTests {
private static final TestIdentity BOC = new TestIdentity(getBOC_NAME());
private static final Party CHARLIE = new TestIdentity(getCHARLIE_NAME(), 90L).getParty();
private static final TestIdentity BOC = new TestIdentity(BOC_NAME);
private static final Party CHARLIE = new TestIdentity(CHARLIE_NAME, 90L).getParty();
private static final TestIdentity DUMMY_CASH_ISSUER_INFO = new TestIdentity(new CordaX500Name("Snake Oil Issuer", "London", "GB"), 10L);
private static final PartyAndReference DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_INFO.ref((byte) 1);
private static final TestIdentity DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L);
private static final TestIdentity DUMMY_NOTARY = new TestIdentity(DUMMY_NOTARY_NAME, 20L);
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
@Rule
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
@ -71,17 +70,17 @@ public class VaultQueryJavaTests {
@Before
public void setUp() throws CertificateException, InvalidAlgorithmParameterException {
List<String> cordappPackages = Arrays.asList(
"net.corda.testing.contracts",
"net.corda.testing.internal.vault",
"net.corda.finance.contracts.asset",
CashSchemaV1.class.getPackage().getName(),
DummyLinearStateSchemaV1.class.getPackage().getName());
IdentityServiceInternal identitySvc = makeTestIdentityService(Arrays.asList(MEGA_CORP.getIdentity(), DUMMY_CASH_ISSUER_INFO.getIdentity(), DUMMY_NOTARY.getIdentity()));
IdentityServiceInternal identitySvc = makeTestIdentityService(MEGA_CORP.getIdentity(), DUMMY_CASH_ISSUER_INFO.getIdentity(), DUMMY_NOTARY.getIdentity());
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(
Arrays.asList(MEGA_CORP.getKey(), DUMMY_NOTARY.getKey()),
identitySvc,
cordappPackages,
MEGA_CORP.getName());
issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_INFO, BOC.getKey());
identitySvc,
MEGA_CORP,
DUMMY_NOTARY.getKeyPair());
issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_INFO, BOC.getKeyPair());
database = databaseAndServices.getFirst();
MockServices services = databaseAndServices.getSecond();
vaultFiller = new VaultFiller(services, DUMMY_NOTARY);
@ -471,16 +470,16 @@ public class VaultQueryJavaTests {
assertThat(results.getOtherResults()).hasSize(12);
assertThat(results.getOtherResults().get(0)).isEqualTo(400L);
assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(BOC.getPubkey()));
assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(BOC.getPublicKey()));
assertThat(results.getOtherResults().get(2)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(3)).isEqualTo(300L);
assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPubkey()));
assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPublicKey()));
assertThat(results.getOtherResults().get(5)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(6)).isEqualTo(200L);
assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(BOC.getPubkey()));
assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(BOC.getPublicKey()));
assertThat(results.getOtherResults().get(8)).isEqualTo("USD");
assertThat(results.getOtherResults().get(9)).isEqualTo(100L);
assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPubkey()));
assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(DUMMY_CASH_ISSUER_INFO.getPublicKey()));
assertThat(results.getOtherResults().get(11)).isEqualTo("USD");
} catch (NoSuchFieldException e) {

View File

@ -17,7 +17,7 @@ import net.corda.node.internal.configureDatabase
import net.corda.testing.TestIdentity
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestIdentityService
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -52,7 +52,7 @@ class InteractiveShellTest {
override fun call() = a
}
private val ids = makeTestIdentityService(listOf(megaCorp.identity))
private val ids = makeTestIdentityService(megaCorp.identity)
private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory())
private fun check(input: String, expected: String) {

View File

@ -0,0 +1,78 @@
package net.corda.node.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.DUMMY_NOTARY_NAME
import net.corda.testing.chooseIdentity
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.node.*
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
import org.junit.Test
import java.nio.file.Path
import kotlin.test.assertFails
class NetworkParametersTest {
private val mockNet = MockNetwork(
emptyList(),
MockNetworkParameters(networkSendManuallyPumped = true),
notarySpecs = listOf(MockNetwork.NotarySpec(DUMMY_NOTARY_NAME)))
@After
fun tearDown() {
mockNet.stopNodes()
}
// Minimum Platform Version tests
@Test
fun `node shutdowns when on lower platform version than network`() {
val alice = mockNet.createUnstartedNode(MockNodeParameters(legalName = ALICE_NAME, forcedID = 100, version = MockServices.MOCK_VERSION_INFO.copy(platformVersion = 1)))
val aliceDirectory = mockNet.baseDirectory(100)
val netParams = testNetworkParameters(
notaries = listOf(NotaryInfo(mockNet.defaultNotaryIdentity, true)),
minimumPlatformVersion = 2)
dropParametersToDir(aliceDirectory, netParams)
assertThatThrownBy { alice.start() }.hasMessageContaining("platform version")
}
@Test
fun `node works fine when on higher platform version`() {
val alice = mockNet.createUnstartedNode(MockNodeParameters(legalName = ALICE_NAME, forcedID = 100, version = MockServices.MOCK_VERSION_INFO.copy(platformVersion = 2)))
val aliceDirectory = mockNet.baseDirectory(100)
val netParams = testNetworkParameters(
notaries = listOf(NotaryInfo(mockNet.defaultNotaryIdentity, true)),
minimumPlatformVersion = 1)
dropParametersToDir(aliceDirectory, netParams)
alice.start()
}
// Notaries tests
@Test
fun `choosing notary not specified in network parameters will fail`() {
val fakeNotary = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME, configOverrides = {
val notary = NotaryConfig(false)
doReturn(notary).whenever(it).notary}))
val fakeNotaryId = fakeNotary.info.chooseIdentity()
val alice = mockNet.createPartyNode(ALICE_NAME)
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
assertFails {
alice.services.startFlow(CashIssueFlow(500.DOLLARS, OpaqueBytes.of(0x01), fakeNotaryId)).resultFuture.getOrThrow()
}
}
// Helpers
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
NetworkParametersCopier(params).install(dir)
}
}

View File

@ -1,7 +1,9 @@
package net.corda.node.messaging
import net.corda.core.messaging.AllPossibleRecipients
import net.corda.node.services.messaging.Message
import net.corda.node.services.messaging.TopicStringValidator
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Before
@ -16,7 +18,7 @@ class InMemoryMessagingTests {
@Before
fun setUp() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
}
@After
@ -72,7 +74,7 @@ class InMemoryMessagingTests {
var counter = 0
listOf(node1, node2, node3).forEach { it.network.addMessageHandler("test.topic") { _, _, _ -> counter++ } }
node1.network.send(node2.network.createMessage("test.topic", data = bits), mockNet.messagingNetwork.everyoneOnline)
node1.network.send(node2.network.createMessage("test.topic", data = bits), rigorousMock<AllPossibleRecipients>())
mockNet.runNetwork(rounds = 1)
assertEquals(3, counter)
}

View File

@ -41,7 +41,12 @@ import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.checkpoints
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.VaultFiller
import net.corda.testing.internal.LogHelper
import net.corda.testing.dsl.LedgerDSL
import net.corda.testing.dsl.TestLedgerDSLInterpreter
import net.corda.testing.dsl.TestTransactionDSLInterpreter
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.After

View File

@ -1,10 +1,19 @@
package net.corda.node.services
import net.corda.core.context.AuthServiceId
import net.corda.core.flows.FlowLogic
import net.corda.core.messaging.CordaRPCOps
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.internal.security.tryAuthenticate
import net.corda.nodeapi.internal.config.User
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import javax.security.auth.login.FailedLoginException
import kotlin.reflect.KFunction
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class RPCSecurityManagerTest {
@ -15,7 +24,147 @@ class RPCSecurityManagerTest {
assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#")
}
private fun configWithRPCUsername(username: String) {
RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
@Test
fun `Generic RPC call authorization`() {
checkUserPermissions(
permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")),
permissions = setOf(
Permissions.invokeRpc(CordaRPCOps::nodeInfo),
Permissions.invokeRpc(CordaRPCOps::notaryIdentities)))
}
@Test
fun `Flow invocation authorization`() {
checkUserPermissions(
permissions = setOf(Permissions.startFlow<DummyFlow>()),
permitted = setOf(
arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"),
arrayListOf("startFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow")))
}
@Test
fun `Check startFlow RPC permission implies startFlowDynamic`() {
checkUserPermissions(
permissions = setOf(Permissions.invokeRpc("startFlow")),
permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic")))
}
@Test
fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() {
checkUserPermissions(
permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")),
permissions = setOf(Permissions.invokeRpc("startTrackedFlow")))
}
@Test
fun `Admin authorization`() {
checkUserPermissions(
permissions = setOf("all"),
permitted = allActions.map { arrayListOf(it) }.toSet())
}
@Test
fun `Malformed permission strings`() {
assertMalformedPermission("bar")
assertMalformedPermission("InvokeRpc.nodeInfo.XXX")
assertMalformedPermission("")
assertMalformedPermission(".")
assertMalformedPermission("..")
assertMalformedPermission("startFlow")
assertMalformedPermission("startFlow.")
}
@Test
fun `Login with unknown user`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "xxxx", emptySet())),
id = AuthServiceId("TEST"))
userRealm.authenticate("user", Password("xxxx"))
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
userRealm.authenticate("foo", Password("xxxx"))
}
assertNull(userRealm.tryAuthenticate("foo", Password("wrong")),
"Login with wrong password should fail")
}
@Test
fun `Login with wrong credentials`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "password", emptySet())),
id = AuthServiceId("TEST"))
userRealm.authenticate("user", Password("password"))
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
userRealm.authenticate("user", Password("wrong"))
}
assertNull(userRealm.tryAuthenticate("user", Password("wrong")),
"Login with wrong password should fail")
}
@Test
fun `Build invalid subject`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "password", emptySet())),
id = AuthServiceId("TEST"))
val subject = userRealm.buildSubject("foo")
for (action in allActions) {
assert(!subject.isPermitted(action)) {
"Invalid subject should not be allowed to call $action"
}
}
}
private fun configWithRPCUsername(username: String) {
RPCSecurityManagerImpl.fromUserList(
users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
}
private fun checkUserPermissions(permissions: Set<String>, permitted: Set<ArrayList<String>>) {
val user = User(username = "user", password = "password", permissions = permissions)
val userRealms = RPCSecurityManagerImpl.fromUserList(users = listOf(user), id = AuthServiceId("TEST"))
val disabled = allActions.filter { !permitted.contains(listOf(it)) }
for (subject in listOf(
userRealms.authenticate("user", Password("password")),
userRealms.tryAuthenticate("user", Password("password"))!!,
userRealms.buildSubject("user"))) {
for (request in permitted) {
val call = request.first()
val args = request.drop(1).toTypedArray()
assert(subject.isPermitted(request.first(), *args)) {
"User ${subject.principal} should be permitted ${call} with target '${request.toList()}'"
}
if (args.isEmpty()) {
assert(subject.isPermitted(request.first(), "XXX")) {
"User ${subject.principal} should be permitted ${call} with any target"
}
}
}
disabled.forEach {
assert(!subject.isPermitted(it)) {
"Permissions $permissions should not allow to call $it"
}
}
disabled.filter { !permitted.contains(listOf(it, "foo")) }.forEach {
assert(!subject.isPermitted(it, "foo")) {
"Permissions $permissions should not allow to call $it with argument 'foo'"
}
}
}
}
private fun assertMalformedPermission(permission: String) {
assertFails {
RPCSecurityManagerImpl.fromUserList(
users = listOf(User("x", "x", setOf(permission))),
id = AuthServiceId("TEST"))
}
}
companion object {
private val allActions = CordaRPCOps::class.members.filterIsInstance<KFunction<*>>().map { it.name }.toSet() +
setOf("startFlow", "startTrackedFlow")
}
private abstract class DummyFlow : FlowLogic<Unit>()
}

View File

@ -12,6 +12,7 @@ import net.corda.core.flows.FlowLogicRefFactory
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.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
@ -23,7 +24,6 @@ import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.services.api.MonitoringService
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.network.NetworkMapCacheImpl
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.services.statemachine.StateMachineManager
@ -31,10 +31,13 @@ import net.corda.node.services.statemachine.StateMachineManagerImpl
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.internal.configureDatabase
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.services.MockAttachmentStorage
@ -43,7 +46,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.nio.file.Paths
import java.time.Clock
import java.time.Instant
import java.util.concurrent.CountDownLatch
@ -53,7 +55,7 @@ import kotlin.test.assertTrue
class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
private companion object {
val ALICE_KEY = TestIdentity(ALICE_NAME, 70).key
val ALICE_KEY = TestIdentity(ALICE_NAME, 70).keyPair
val DUMMY_IDENTITY_1 = getTestPartyAndCertificate(Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public))
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val myInfo = NodeInfo(listOf(NetworkHostAndPort("mockHost", 30000)), listOf(DUMMY_IDENTITY_1), 1, serial = 1L)
@ -98,14 +100,19 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
database = configureDatabase(dataSourceProps, DatabaseConfig(), rigorousMock())
val identityService = makeTestIdentityService()
kms = MockKeyManagementService(identityService, ALICE_KEY)
val configuration = testNodeConfiguration(Paths.get("."), CordaX500Name("Alice", "London", "GB"))
val configuration = rigorousMock<NodeConfiguration>().also {
doReturn(true).whenever(it).devMode
doReturn(null).whenever(it).devModeOptions
}
val validatedTransactions = MockTransactionStorage()
database.transaction {
services = rigorousMock<Services>().also {
doReturn(configuration).whenever(it).configuration
doReturn(MonitoringService(MetricRegistry())).whenever(it).monitoringService
doReturn(validatedTransactions).whenever(it).validatedTransactions
doReturn(NetworkMapCacheImpl(MockNetworkMapCache(database), identityService)).whenever(it).networkMapCache
doReturn(rigorousMock<NetworkMapCacheInternal>().also {
doReturn(doneFuture(null)).whenever(it).nodeReady
}).whenever(it).networkMapCache
doReturn(myInfo).whenever(it).myInfo
doReturn(kms).whenever(it).keyManagementService
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts")), MockAttachmentStorage())).whenever(it).cordappProvider

View File

@ -13,6 +13,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.*
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@ -27,13 +28,17 @@ class InMemoryIdentityServiceTests {
val bob = TestIdentity(BOB_NAME, 80)
val ALICE get() = alice.party
val ALICE_IDENTITY get() = alice.identity
val ALICE_PUBKEY get() = alice.pubkey
val ALICE_PUBKEY get() = alice.publicKey
val BOB get() = bob.party
val BOB_IDENTITY get() = bob.identity
val BOB_PUBKEY get() = bob.pubkey
fun createService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities.toSet(), DEV_TRUST_ROOT)
val BOB_PUBKEY get() = bob.publicKey
fun createService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities, DEV_TRUST_ROOT)
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@Test
fun `get all identities`() {
val service = createService()
@ -94,18 +99,16 @@ class InMemoryIdentityServiceTests {
*/
@Test
fun `assert unknown anonymous key is unrecognised`() {
withTestSerialization {
val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey)
val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val service = createService()
// TODO: Generate certificate with an EdDSA key rather than ECDSA
val identity = Party(rootCert.cert)
val txIdentity = AnonymousParty(txKey.public)
val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey)
val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val service = createService()
// TODO: Generate certificate with an EdDSA key rather than ECDSA
val identity = Party(rootCert.cert)
val txIdentity = AnonymousParty(txKey.public)
assertFailsWith<UnknownAnonymousPartyException> {
service.assertOwnership(identity, txIdentity)
}
assertFailsWith<UnknownAnonymousPartyException> {
service.assertOwnership(identity, txIdentity)
}
}
@ -137,30 +140,28 @@ class InMemoryIdentityServiceTests {
*/
@Test
fun `assert ownership`() {
withTestSerialization {
val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name)
val (bob, anonymousBob) = createConfidentialIdentity(BOB.name)
val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name)
val (bob, anonymousBob) = createConfidentialIdentity(BOB.name)
// Now we have identities, construct the service and let it know about both
val service = createService(alice, bob)
service.verifyAndRegisterIdentity(anonymousAlice)
service.verifyAndRegisterIdentity(anonymousBob)
// Now we have identities, construct the service and let it know about both
val service = createService(alice, bob)
service.verifyAndRegisterIdentity(anonymousAlice)
service.verifyAndRegisterIdentity(anonymousBob)
// Verify that paths are verified
service.assertOwnership(alice.party, anonymousAlice.party.anonymise())
service.assertOwnership(bob.party, anonymousBob.party.anonymise())
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(alice.party, anonymousBob.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(bob.party, anonymousAlice.party.anonymise())
}
// Verify that paths are verified
service.assertOwnership(alice.party, anonymousAlice.party.anonymise())
service.assertOwnership(bob.party, anonymousBob.party.anonymise())
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(alice.party, anonymousBob.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(bob.party, anonymousAlice.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded)
val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal)
service.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded)
val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal)
service.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise())
}
}

View File

@ -21,6 +21,7 @@ import net.corda.testing.node.MockServices.Companion.makeTestDataSourcePropertie
import net.corda.testing.node.makeTestIdentityService
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@ -35,12 +36,15 @@ class PersistentIdentityServiceTests {
val bob = TestIdentity(BOB_NAME, 80)
val ALICE get() = alice.party
val ALICE_IDENTITY get() = alice.identity
val ALICE_PUBKEY get() = alice.pubkey
val ALICE_PUBKEY get() = alice.publicKey
val BOB get() = bob.party
val BOB_IDENTITY get() = bob.identity
val BOB_PUBKEY get() = bob.pubkey
val BOB_PUBKEY get() = bob.publicKey
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private lateinit var database: CordaPersistence
private lateinit var identityService: IdentityService
@ -138,17 +142,15 @@ class PersistentIdentityServiceTests {
*/
@Test
fun `assert unknown anonymous key is unrecognised`() {
withTestSerialization {
val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey)
val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME)
val identity = Party(rootCert.cert)
val txIdentity = AnonymousParty(txKey.public)
val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey)
val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME)
val identity = Party(rootCert.cert)
val txIdentity = AnonymousParty(txKey.public)
assertFailsWith<UnknownAnonymousPartyException> {
database.transaction {
identityService.assertOwnership(identity, txIdentity)
}
assertFailsWith<UnknownAnonymousPartyException> {
database.transaction {
identityService.assertOwnership(identity, txIdentity)
}
}
}
@ -191,38 +193,36 @@ class PersistentIdentityServiceTests {
*/
@Test
fun `assert ownership`() {
withTestSerialization {
val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name)
val (bob, anonymousBob) = createConfidentialIdentity(BOB.name)
val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name)
val (bob, anonymousBob) = createConfidentialIdentity(BOB.name)
database.transaction {
// Now we have identities, construct the service and let it know about both
identityService.verifyAndRegisterIdentity(anonymousAlice)
identityService.verifyAndRegisterIdentity(anonymousBob)
}
// Verify that paths are verified
database.transaction {
identityService.assertOwnership(alice.party, anonymousAlice.party.anonymise())
identityService.assertOwnership(bob.party, anonymousBob.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
database.transaction {
// Now we have identities, construct the service and let it know about both
identityService.verifyAndRegisterIdentity(anonymousAlice)
identityService.verifyAndRegisterIdentity(anonymousBob)
identityService.assertOwnership(alice.party, anonymousBob.party.anonymise())
}
// Verify that paths are verified
}
assertFailsWith<IllegalArgumentException> {
database.transaction {
identityService.assertOwnership(alice.party, anonymousAlice.party.anonymise())
identityService.assertOwnership(bob.party, anonymousBob.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
database.transaction {
identityService.assertOwnership(alice.party, anonymousBob.party.anonymise())
}
}
assertFailsWith<IllegalArgumentException> {
database.transaction {
identityService.assertOwnership(bob.party, anonymousAlice.party.anonymise())
}
identityService.assertOwnership(bob.party, anonymousAlice.party.anonymise())
}
}
assertFailsWith<IllegalArgumentException> {
val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded)
database.transaction {
val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal)
identityService.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise())
}
assertFailsWith<IllegalArgumentException> {
val owningKey = Crypto.decodePublicKey(DEV_CA.certificate.subjectPublicKeyInfo.encoded)
database.transaction {
val subject = CordaX500Name.build(DEV_CA.certificate.cert.subjectX500Principal)
identityService.assertOwnership(Party(subject, owningKey), anonymousAlice.party.anonymise())
}
}
}

View File

@ -1,5 +1,7 @@
package net.corda.node.services.messaging
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.context.AuthServiceId
import net.corda.core.crypto.generateKeyPair
import net.corda.core.concurrent.CordaFuture
@ -7,21 +9,23 @@ import com.codahale.metrics.MetricRegistry
import net.corda.core.crypto.generateKeyPair
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.configureDatabase
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.network.NetworkMapCacheImpl
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.testNodeConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
@ -65,9 +69,17 @@ class ArtemisMessagingTests {
@Before
fun setUp() {
securityManager = RPCSecurityManagerImpl.fromUserList(users = emptyList(), id = AuthServiceId("TEST"))
config = testNodeConfiguration(
baseDirectory = temporaryFolder.root.toPath(),
myLegalName = ALICE_NAME)
abstract class AbstractNodeConfiguration : NodeConfiguration
config = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory
doReturn(ALICE_NAME).whenever(it).myLegalName
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn("").whenever(it).exportJMXto
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(5).whenever(it).messageRedeliveryDelaySeconds
doReturn(true).whenever(it).useAMQPBridges
}
LogHelper.setLevel(PersistentUniquenessProvider::class)
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock())
networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, emptyList()), rigorousMock())
@ -159,7 +171,7 @@ class ArtemisMessagingTests {
return Pair(messagingClient, receivedMessages)
}
private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort), platformVersion: Int = 1): P2PMessagingClient {
private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort), platformVersion: Int = 1, maxMessageSize: Int = MAX_MESSAGE_SIZE): P2PMessagingClient {
return database.transaction {
P2PMessagingClient(
config,
@ -167,16 +179,16 @@ class ArtemisMessagingTests {
server,
identity.public,
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
database
).apply {
database,
maxMessageSize = maxMessageSize).apply {
config.configureWithDevSSLCertificate()
messagingClient = this
}
}
}
private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort): ArtemisMessagingServer {
return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager).apply {
private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort, maxMessageSize: Int = MAX_MESSAGE_SIZE): ArtemisMessagingServer {
return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager, maxMessageSize).apply {
config.configureWithDevSSLCertificate()
messagingServer = this
}

View File

@ -13,7 +13,7 @@ import java.math.BigInteger
import kotlin.test.assertEquals
class NetworkMapCacheTest {
private val mockNet = MockNetwork()
private val mockNet = MockNetwork(emptyList())
@After
fun teardown() {

View File

@ -5,14 +5,13 @@ import net.corda.core.crypto.sha256
import net.corda.core.internal.cert
import net.corda.core.serialization.serialize
import net.corda.core.utilities.seconds
import net.corda.node.services.network.TestNodeInfoFactory.createNodeInfo
import net.corda.testing.DEV_CA
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.DEV_TRUST_ROOT
import net.corda.testing.ROOT_CA
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.network.NetworkMapServer
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.internal.createNodeInfoAndSigned
import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
@ -26,13 +25,12 @@ class NetworkMapClientTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val cacheTimeout = 100000.seconds
private lateinit var server: NetworkMapServer
private lateinit var networkMapClient: NetworkMapClient
companion object {
private val cacheTimeout = 100000.seconds
}
@Before
fun setUp() {
server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort())
@ -47,9 +45,7 @@ class NetworkMapClientTest {
@Test
fun `registered node is added to the network map`() {
// Create node info.
val signedNodeInfo = createNodeInfo("Test1")
val nodeInfo = signedNodeInfo.verified()
val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
networkMapClient.publish(signedNodeInfo)
@ -58,8 +54,8 @@ class NetworkMapClientTest {
assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash)
assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash))
val signedNodeInfo2 = createNodeInfo("Test2")
val nodeInfo2 = signedNodeInfo2.verified()
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned(BOB_NAME)
networkMapClient.publish(signedNodeInfo2)
val nodeInfoHash2 = nodeInfo2.serialize().sha256()
@ -72,7 +68,7 @@ class NetworkMapClientTest {
@Test
fun `download NetworkParameter correctly`() {
// The test server returns same network parameter for any hash.
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
assertNotNull(networkParameter)
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter)
}

View File

@ -1,65 +1,70 @@
package net.corda.node.services.network
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Configuration.unix
import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import net.corda.cordform.CordformNode
import net.corda.core.crypto.Crypto
import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
import net.corda.nodeapi.internal.NetworkMap
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.millis
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.testing.ALICE_NAME
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import rx.schedulers.TestScheduler
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
class NetworkMapUpdaterTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val jimFs = Jimfs.newFileSystem(Configuration.unix())
private val baseDir = jimFs.getPath("/node")
private val fs = Jimfs.newFileSystem(unix())
private val baseDir = fs.getPath("/node")
private val networkMapCache = createMockNetworkMapCache()
private val nodeInfoMap = ConcurrentHashMap<SecureHash, SignedNodeInfo>()
private val cacheExpiryMs = 100
private val networkMapClient = createMockNetworkMapClient()
private val scheduler = TestScheduler()
private val networkParametersHash = SecureHash.randomSHA256()
private val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
private val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, networkParametersHash)
private val nodeInfoBuilder = TestNodeInfoBuilder()
@After
fun cleanUp() {
updater.close()
fs.close()
}
@Test
fun `publish node info`() {
val keyPair = Crypto.generateKeyPair()
nodeInfoBuilder.addIdentity(ALICE_NAME)
val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1").verified()
val signedNodeInfo = TestNodeInfoFactory.sign(keyPair, nodeInfo1)
val sameNodeInfoDifferentTime = nodeInfo1.copy(serial = System.currentTimeMillis())
val signedSameNodeInfoDifferentTime = TestNodeInfoFactory.sign(keyPair, sameNodeInfoDifferentTime)
val differentNodeInfo = nodeInfo1.copy(addresses = listOf(NetworkHostAndPort("my.new.host.com", 1000)))
val signedDifferentNodeInfo = TestNodeInfoFactory.sign(keyPair, differentNodeInfo)
val networkMapCache = getMockNetworkMapCache()
val networkMapClient = mock<NetworkMapClient>()
val scheduler = TestScheduler()
val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient)
val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned()
val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis())
// Publish node info for the first time.
updater.updateNodeInfo(nodeInfo1) { signedNodeInfo }
updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 }
// Sleep as publish is asynchronous.
// TODO: Remove sleep in unit test
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
verify(networkMapClient, times(1)).publish(any())
networkMapCache.addNode(nodeInfo1)
@ -67,167 +72,144 @@ class NetworkMapUpdaterTest {
// Publish the same node info, but with different serial.
updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime }
// TODO: Remove sleep in unit test.
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
// Same node info should not publish twice
verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime)
val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob")
// Publish different node info.
updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo }
// TODO: Remove sleep in unit test.
Thread.sleep(200)
verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo)
updater.close()
}
@Test
fun `process add node updates from network map, with additional node infos from dir`() {
val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1")
val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2")
val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3")
val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4")
val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file")
val networkMapCache = getMockNetworkMapCache()
val nodeInfoMap = ConcurrentHashMap<SecureHash, SignedData<NodeInfo>>()
val networkMapClient = mock<NetworkMapClient> {
on { publish(any()) }.then {
val signedNodeInfo: SignedData<NodeInfo> = uncheckedCast(it.arguments.first())
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) }
on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() }
}
val scheduler = TestScheduler()
val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient)
val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1")
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
// Test adding new node.
networkMapClient.publish(nodeInfo1)
networkMapClient.publish(signedNodeInfo1)
// Not subscribed yet.
verify(networkMapCache, times(0)).addNode(any())
updater.subscribeToNetworkMap()
networkMapClient.publish(nodeInfo2)
networkMapClient.publish(signedNodeInfo2)
// TODO: Remove sleep in unit test.
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
verify(networkMapCache, times(2)).addNode(any())
verify(networkMapCache, times(1)).addNode(nodeInfo1.verified())
verify(networkMapCache, times(1)).addNode(nodeInfo2.verified())
verify(networkMapCache, times(1)).addNode(nodeInfo1)
verify(networkMapCache, times(1)).addNode(nodeInfo2)
NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo)
networkMapClient.publish(nodeInfo3)
networkMapClient.publish(nodeInfo4)
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
networkMapClient.publish(signedNodeInfo3)
networkMapClient.publish(signedNodeInfo4)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
// TODO: Remove sleep in unit test.
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
// 4 node info from network map, and 1 from file.
verify(networkMapCache, times(5)).addNode(any())
verify(networkMapCache, times(1)).addNode(nodeInfo3.verified())
verify(networkMapCache, times(1)).addNode(nodeInfo4.verified())
verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified())
updater.close()
verify(networkMapCache, times(1)).addNode(nodeInfo3)
verify(networkMapCache, times(1)).addNode(nodeInfo4)
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
}
@Test
fun `process remove node updates from network map, with additional node infos from dir`() {
val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1")
val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2")
val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3")
val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4")
val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file")
val networkMapCache = getMockNetworkMapCache()
val nodeInfoMap = ConcurrentHashMap<SecureHash, SignedData<NodeInfo>>()
val networkMapClient = mock<NetworkMapClient> {
on { publish(any()) }.then {
val signedNodeInfo: SignedData<NodeInfo> = uncheckedCast(it.arguments.first())
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) }
on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() }
}
val scheduler = TestScheduler()
val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient)
val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1")
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
// Add all nodes.
NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo)
networkMapClient.publish(nodeInfo1)
networkMapClient.publish(nodeInfo2)
networkMapClient.publish(nodeInfo3)
networkMapClient.publish(nodeInfo4)
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
networkMapClient.publish(signedNodeInfo1)
networkMapClient.publish(signedNodeInfo2)
networkMapClient.publish(signedNodeInfo3)
networkMapClient.publish(signedNodeInfo4)
updater.subscribeToNetworkMap()
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
// TODO: Remove sleep in unit test.
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
// 4 node info from network map, and 1 from file.
assertEquals(4, nodeInfoMap.size)
assertThat(nodeInfoMap).hasSize(4)
verify(networkMapCache, times(5)).addNode(any())
verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified())
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
// Test remove node.
nodeInfoMap.clear()
// TODO: Remove sleep in unit test.
Thread.sleep(200)
Thread.sleep(2L * cacheExpiryMs)
verify(networkMapCache, times(4)).removeNode(any())
verify(networkMapCache, times(1)).removeNode(nodeInfo1.verified())
verify(networkMapCache, times(1)).removeNode(nodeInfo2.verified())
verify(networkMapCache, times(1)).removeNode(nodeInfo3.verified())
verify(networkMapCache, times(1)).removeNode(nodeInfo4.verified())
verify(networkMapCache, times(1)).removeNode(nodeInfo1)
verify(networkMapCache, times(1)).removeNode(nodeInfo2)
verify(networkMapCache, times(1)).removeNode(nodeInfo3)
verify(networkMapCache, times(1)).removeNode(nodeInfo4)
// Node info from file should not be deleted
assertEquals(1, networkMapCache.allNodeHashes.size)
assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first())
updater.close()
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash)
}
@Test
fun `receive node infos from directory, without a network map`() {
val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file")
val networkMapCache = getMockNetworkMapCache()
val scheduler = TestScheduler()
val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
val updater = NetworkMapUpdater(networkMapCache, fileWatcher, null)
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
// Not subscribed yet.
verify(networkMapCache, times(0)).addNode(any())
updater.subscribeToNetworkMap()
NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo)
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).addNode(any())
verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified())
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
assertEquals(1, networkMapCache.allNodeHashes.size)
assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first())
updater.close()
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash)
}
private fun getMockNetworkMapCache() = mock<NetworkMapCacheInternal> {
val data = ConcurrentHashMap<Party, NodeInfo>()
on { addNode(any()) }.then {
val nodeInfo = it.arguments.first() as NodeInfo
data.put(nodeInfo.legalIdentities.first(), nodeInfo)
private fun createMockNetworkMapClient(): NetworkMapClient {
return mock {
on { publish(any()) }.then {
val signedNodeInfo: SignedNodeInfo = uncheckedCast(it.arguments[0])
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then {
NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), networkParametersHash), cacheExpiryMs.millis)
}
on { getNodeInfo(any()) }.then {
nodeInfoMap[it.arguments[0]]?.verified()
}
}
on { removeNode(any()) }.then { data.remove((it.arguments.first() as NodeInfo).legalIdentities.first()) }
on { getNodeByLegalIdentity(any()) }.then { data[it.arguments.first()] }
on { allNodeHashes }.then { data.values.map { it.serialize().hash } }
on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments.first() } }
}
private fun createMockNetworkMapCache(): NetworkMapCacheInternal {
return mock {
val data = ConcurrentHashMap<Party, NodeInfo>()
on { addNode(any()) }.then {
val nodeInfo = it.arguments[0] as NodeInfo
data.put(nodeInfo.legalIdentities[0], nodeInfo)
}
on { removeNode(any()) }.then { data.remove((it.arguments[0] as NodeInfo).legalIdentities[0]) }
on { getNodeByLegalIdentity(any()) }.then { data[it.arguments[0]] }
on { allNodeHashes }.then { data.values.map { it.serialize().hash } }
on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments[0] } }
}
}
private fun createNodeInfoAndSigned(org: String): Pair<NodeInfo, SignedNodeInfo> {
return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB"))
}
}

View File

@ -1,49 +0,0 @@
package net.corda.node.services.network
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SignedData
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.KeyPair
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.X509Certificate
object TestNodeInfoFactory {
private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", organisation = "R3 LTD", locality = "London", country = "GB"), rootCAKey)
private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
fun createNodeInfo(organisation: String): SignedData<NodeInfo> {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public)
val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.$organisation.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
return sign(keyPair, nodeInfo)
}
fun <T : Any> sign(keyPair: KeyPair, t: T): SignedData<T> {
// Create digital signature.
val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, t.serialize().bytes))
return SignedData(t.serialize(), digitalSignature)
}
private fun buildCertPath(vararg certificates: Certificate): CertPath {
return X509CertificateFactory().generateCertPath(*certificates)
}
private fun X509CertificateHolder.toX509Certificate(): X509Certificate {
return X509CertificateFactory().generateCertificate(encoded.inputStream())
}
}

View File

@ -3,7 +3,6 @@ package net.corda.node.services.persistence
import net.corda.core.context.InvocationContext
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.serialize
@ -15,7 +14,10 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before

View File

@ -13,6 +13,8 @@ import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
@ -24,7 +26,7 @@ import kotlin.test.assertEquals
class DBTransactionStorageTests {
private companion object {
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
}

View File

@ -37,12 +37,12 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.testing.*
import net.corda.testing.contracts.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.schemas.DummyDealStateSchemaV1
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import net.corda.testing.schemas.DummyLinearStateSchemaV2
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
import net.corda.testing.internal.vault.DummyLinearStateSchemaV2
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.hibernate.SessionFactory
@ -62,7 +62,7 @@ class HibernateConfigurationTest {
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val BOC get() = bankOfCorda.party
val BOC_KEY get() = bankOfCorda.key
val BOC_KEY get() = bankOfCorda.keyPair
}
@Rule
@ -93,7 +93,7 @@ class HibernateConfigurationTest {
@Before
fun setUp() {
val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset")
val cordappPackages = listOf("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset")
bankServices = MockServices(cordappPackages, rigorousMock(), BOC.name, BOC_KEY)
issuerServices = MockServices(cordappPackages, rigorousMock(), dummyCashIssuer)
notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary)
@ -113,7 +113,7 @@ class HibernateConfigurationTest {
// `consumeCash` expects we can self-notarise transactions
services = object : MockServices(cordappPackages, rigorousMock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME })
}, BOB_NAME, generateKeyPair(), dummyNotary.key) {
}, BOB_NAME, generateKeyPair(), dummyNotary.keyPair) {
override val vaultService = makeVaultService(database.hibernateConfig, schemaService)
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) {

View File

@ -17,9 +17,9 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.LogHelper
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.junit.After
import org.junit.Before
import org.junit.Ignore

View File

@ -16,11 +16,11 @@ import net.corda.node.services.api.SchemaService
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.testing.LogHelper
import net.corda.testing.internal.LogHelper
import net.corda.testing.TestIdentity
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.junit.After
import org.junit.Before
import org.junit.Test

View File

@ -12,7 +12,7 @@ import net.corda.node.services.api.ServiceHubInternal
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
import net.corda.testing.node.MockNetwork
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
import org.hibernate.annotations.Cascade
import org.hibernate.annotations.CascadeType
import org.junit.Test

View File

@ -32,6 +32,7 @@ import net.corda.node.services.persistence.checkpoints
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork

View File

@ -13,11 +13,11 @@ import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.LogHelper
import net.corda.testing.internal.LogHelper
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.junit.After
import org.junit.Before
import org.junit.Rule

View File

@ -7,6 +7,8 @@ import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.junit.After
import org.junit.Before

View File

@ -31,7 +31,9 @@ import net.corda.finance.utils.sumCash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.VaultFiller
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions.assertThat
@ -64,8 +66,8 @@ class NodeVaultServiceTest {
val DUMMY_NOTARY get() = dummyNotary.party
val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
val MEGA_CORP get() = megaCorp.party
val MEGA_CORP_KEY get() = megaCorp.key
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
val MEGA_CORP_KEY get() = megaCorp.keyPair
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
val MEGA_CORP_IDENTITY get() = megaCorp.identity
val MINI_CORP get() = miniCorp.party
val MINI_CORP_IDENTITY get() = miniCorp.identity
@ -86,10 +88,9 @@ class NodeVaultServiceTest {
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
val databaseAndServices = MockServices.makeTestDatabaseAndMockServices(
listOf(MEGA_CORP_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
cordappPackages,
MEGA_CORP.name)
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
megaCorp)
database = databaseAndServices.first
services = databaseAndServices.second
vaultFiller = VaultFiller(services, dummyNotary)
@ -136,7 +137,7 @@ class NodeVaultServiceTest {
assertThat(w1).hasSize(3)
val originalVault = vaultService
val services2 = object : MockServices(rigorousMock(), MEGA_CORP.name) {
val services2 = object : MockServices(emptyList(), rigorousMock(), MEGA_CORP.name) {
override val vaultService: NodeVaultService get() = originalVault
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) {
@ -586,7 +587,7 @@ class NodeVaultServiceTest {
val identity = services.myInfo.singleIdentityAndCert()
assertEquals(services.identityService.partyFromKey(identity.owningKey), identity.party)
val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false)
val thirdPartyServices = MockServices(rigorousMock<IdentityServiceInternal>().also {
val thirdPartyServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MEGA_CORP.name })
}, MEGA_CORP.name)
val thirdPartyIdentity = thirdPartyServices.keyManagementService.freshKeyAndCert(thirdPartyServices.myInfo.singleIdentityAndCert(), false)

View File

@ -26,12 +26,15 @@ import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
import net.corda.testing.contracts.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID
import net.corda.testing.internal.vault.DummyLinearContract
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestIdentityService
import net.corda.testing.schemas.DummyDealStateSchemaV1
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.*
@ -65,17 +68,17 @@ open class VaultQueryTests {
val BOB_IDENTITY get() = bob.identity
val BOC get() = bankOfCorda.party
val BOC_IDENTITY get() = bankOfCorda.identity
val BOC_KEY get() = bankOfCorda.key
val BOC_PUBKEY get() = bankOfCorda.pubkey
val BOC_KEY get() = bankOfCorda.keyPair
val BOC_PUBKEY get() = bankOfCorda.publicKey
val CASH_NOTARY get() = cashNotary.party
val CASH_NOTARY_IDENTITY get() = cashNotary.identity
val CHARLIE get() = charlie.party
val CHARLIE_IDENTITY get() = charlie.identity
val DUMMY_NOTARY get() = dummyNotary.party
val DUMMY_NOTARY_KEY get() = dummyNotary.key
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
val MEGA_CORP_IDENTITY get() = megaCorp.identity
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
val MEGA_CORP_KEY get() = megaCorp.key
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
val MEGA_CORP_KEY get() = megaCorp.keyPair
val MEGA_CORP get() = megaCorp.party
val MINI_CORP_IDENTITY get() = miniCorp.identity
val MINI_CORP get() = miniCorp.party
@ -105,15 +108,15 @@ open class VaultQueryTests {
open fun setUp() {
// register additional identities
val databaseAndServices = makeTestDatabaseAndMockServices(
listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity)),
cordappPackages,
MEGA_CORP.name)
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity),
megaCorp,
DUMMY_NOTARY_KEY)
database = databaseAndServices.first
services = databaseAndServices.second
vaultFiller = VaultFiller(services, dummyNotary)
vaultFillerCashNotary = VaultFiller(services, dummyNotary, CASH_NOTARY)
notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary, dummyCashIssuer.key, BOC_KEY, MEGA_CORP_KEY)
notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary, dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY)
identitySvc = services.identityService
// Register all of the identities we're going to use
(notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity ->

View File

@ -28,7 +28,7 @@ import net.corda.node.services.api.VaultServiceInternal
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.testing.chooseIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.startFlow
import org.junit.After

View File

@ -21,11 +21,12 @@ import net.corda.finance.contracts.getCashBalance
import net.corda.finance.schemas.CashSchemaV1
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestIdentityService
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
@ -41,7 +42,7 @@ import kotlin.test.fail
class VaultWithCashTest {
private companion object {
private val cordappPackages = listOf(
"net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName)
"net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName)
val BOB = TestIdentity(BOB_NAME, 80).party
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1)
@ -51,13 +52,14 @@ class VaultWithCashTest {
val DUMMY_NOTARY get() = dummyNotary.party
val MEGA_CORP get() = megaCorp.party
val MEGA_CORP_IDENTITY get() = megaCorp.identity
val MEGA_CORP_KEY get() = megaCorp.key
val MEGA_CORP_KEY get() = megaCorp.keyPair
val MINI_CORP_IDENTITY get() = miniCorp.identity
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val servicesKey = generateKeyPair()
lateinit var services: MockServices
private lateinit var vaultFiller: VaultFiller
lateinit var issuerServices: MockServices
@ -70,10 +72,10 @@ class VaultWithCashTest {
fun setUp() {
LogHelper.setLevel(VaultWithCashTest::class)
val databaseAndServices = makeTestDatabaseAndMockServices(
listOf(generateKeyPair(), dummyNotary.key),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity)),
cordappPackages,
MEGA_CORP.name)
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity),
TestIdentity(MEGA_CORP.name, servicesKey),
dummyNotary.keyPair)
database = databaseAndServices.first
services = databaseAndServices.second
vaultFiller = VaultFiller(services, dummyNotary)
@ -100,8 +102,7 @@ class VaultWithCashTest {
val state = w[0].state.data
assertEquals(30.45.DOLLARS `issued by` DUMMY_CASH_ISSUER, state.amount)
assertEquals(services.key.public, state.owner.owningKey)
assertEquals(servicesKey.public, state.owner.owningKey)
assertEquals(34.70.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[2].state.data).amount)
assertEquals(34.85.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[1].state.data).amount)
}

View File

@ -6,7 +6,7 @@ import net.corda.core.internal.tee
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test

View File

@ -14,8 +14,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.getX509Certificate
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.ALICE_NAME
import net.corda.testing.rigorousMock
import net.corda.testing.node.testNodeConfiguration
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
@ -45,7 +44,14 @@ class NetworkRegistrationHelperTest {
@Before
fun init() {
config = testNodeConfiguration(baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE_NAME)
abstract class AbstractNodeConfiguration : NodeConfiguration
config = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn(ALICE_NAME).whenever(it).myLegalName
doReturn("").whenever(it).emailAddress
}
}
@Test