mirror of
https://github.com/corda/corda.git
synced 2025-06-16 14:18:20 +00:00
Merge remote-tracking branch 'open/master' into colljos-os-merge-rc01
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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?>
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.corda.node.internal.protonwrapper.messages
|
||||
|
||||
/**
|
||||
* The processing state of a message.
|
||||
*/
|
||||
enum class MessageStatus {
|
||||
Unsent,
|
||||
Sent,
|
||||
Acknowledged,
|
||||
Rejected
|
||||
}
|
@ -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)
|
||||
}
|
@ -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>
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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)
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -33,3 +33,5 @@ enterpriseConfiguration = {
|
||||
waitInterval = 40000
|
||||
}
|
||||
}
|
||||
|
||||
useAMQPBridges = true
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>()
|
||||
}
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user