Added security to RPC and P2P systems.

This commit is contained in:
Shams Asari
2016-11-07 14:30:40 +00:00
parent b6e56641bd
commit 4addb91f80
29 changed files with 875 additions and 415 deletions

View File

@ -9,6 +9,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLI
import net.corda.node.services.messaging.CordaRPCOps import net.corda.node.services.messaging.CordaRPCOps
import net.corda.node.services.messaging.RPCException import net.corda.node.services.messaging.RPCException
import net.corda.node.services.messaging.rpcLog import net.corda.node.services.messaging.rpcLog
import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientSession import org.apache.activemq.artemis.api.core.client.ClientSession
@ -38,11 +39,11 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
private val state = ThreadBox(State()) private val state = ThreadBox(State())
/** Opens the connection to the server and registers a JVM shutdown hook to cleanly disconnect. */ /** Opens the connection to the server and registers a JVM shutdown hook to cleanly disconnect. */
@Throws(ActiveMQNotConnectedException::class) @Throws(ActiveMQException::class)
fun start(username: String, password: String) { fun start(username: String, password: String) {
state.locked { state.locked {
check(!running) check(!running)
checkStorePasswords() // Check the password. checkStorePasswords()
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.OUTBOUND, host.hostText, host.port)) val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.OUTBOUND, host.hostText, host.port))
serverLocator.threadPoolMaxSize = 1 serverLocator.threadPoolMaxSize = 1
// TODO: Configure session reconnection, confirmation window sizes and other Artemis features. // TODO: Configure session reconnection, confirmation window sizes and other Artemis features.
@ -53,7 +54,6 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
session.start() session.start()
clientImpl = CordaRPCClientImpl(session, state.lock, username) clientImpl = CordaRPCClientImpl(session, state.lock, username)
running = true running = true
// We will use the ID in strings so strip the sign bit.
} }
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {

View File

@ -41,7 +41,7 @@ import kotlin.reflect.jvm.javaMethod
* # Design notes * # Design notes
* *
* The way RPCs are handled is fairly standard except for the handling of observables. When an RPC might return * The way RPCs are handled is fairly standard except for the handling of observables. When an RPC might return
* an [rx.Observable] it is specially tagged. This causes the client to create a new transient queue for the * an [Observable] it is specially tagged. This causes the client to create a new transient queue for the
* receiving of observables and their observations with a random ID in the name. This ID is sent to the server in * receiving of observables and their observations with a random ID in the name. This ID is sent to the server in
* a message header. All observations are sent via this single queue. * a message header. All observations are sent via this single queue.
* *
@ -141,19 +141,20 @@ class CordaRPCClientImpl(private val session: ClientSession,
*/ */
@ThreadSafe @ThreadSafe
private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable { private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable {
private val proxyAddress = constructAddress() private val proxyId = random63BitValue()
private val consumer: ClientConsumer private val consumer: ClientConsumer
var serverProtocolVersion = 0 var serverProtocolVersion = 0
init { init {
val proxyAddress = constructAddress(proxyId)
consumer = sessionLock.withLock { consumer = sessionLock.withLock {
session.createTemporaryQueue(proxyAddress, proxyAddress) session.createTemporaryQueue(proxyAddress, proxyAddress)
session.createConsumer(proxyAddress) session.createConsumer(proxyAddress)
} }
} }
private fun constructAddress() = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$username.rpc.${random63BitValue()}" private fun constructAddress(addressId: Long) = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$username.rpc.$addressId"
@Synchronized @Synchronized
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? { override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
@ -230,9 +231,10 @@ class CordaRPCClientImpl(private val session: ClientSession,
private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo { private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo {
// Create a temporary queue just for the emissions on any observables that are returned. // Create a temporary queue just for the emissions on any observables that are returned.
val observationsQueueName = constructAddress() val observationsId = random63BitValue()
val observationsQueueName = constructAddress(observationsId)
session.createTemporaryQueue(observationsQueueName, observationsQueueName) session.createTemporaryQueue(observationsQueueName, observationsQueueName)
msg.putStringProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, observationsQueueName) msg.putLongProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, observationsId)
// And make sure that we deserialise observable handles so that they're linked to the right // And make sure that we deserialise observable handles so that they're linked to the right
// queue. Also record a bit of metadata for debugging purposes. // queue. Also record a bit of metadata for debugging purposes.
return createRPCKryo(observableSerializer = ObservableDeserializer(observationsQueueName, method.name, location)) return createRPCKryo(observableSerializer = ObservableDeserializer(observationsQueueName, method.name, location))
@ -241,7 +243,7 @@ class CordaRPCClientImpl(private val session: ClientSession,
private fun createMessage(method: Method): ClientMessage { private fun createMessage(method: Method): ClientMessage {
return session.createMessage(false).apply { return session.createMessage(false).apply {
putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name) putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name)
putStringProperty(ClientRPCRequestMessage.REPLY_TO, proxyAddress) putLongProperty(ClientRPCRequestMessage.REPLY_TO, proxyId)
// Use the magic deduplication property built into Artemis as our message identity too // Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
} }
@ -257,10 +259,10 @@ class CordaRPCClientImpl(private val session: ClientSession,
override fun close() { override fun close() {
consumer.close() consumer.close()
sessionLock.withLock { session.deleteQueue(proxyAddress) } sessionLock.withLock { session.deleteQueue(constructAddress(proxyId)) }
} }
override fun toString() = "Corda RPC Proxy listening on queue $proxyAddress" override fun toString() = "Corda RPC Proxy listening on queue ${constructAddress(proxyId)}"
} }
/** /**

View File

@ -1,8 +1,6 @@
package net.corda.client package net.corda.client
import net.corda.client.impl.CordaRPCClientImpl import net.corda.client.impl.CordaRPCClientImpl
import net.corda.core.millis
import net.corda.core.random63BitValue
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.LogHelper import net.corda.core.utilities.LogHelper
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
@ -22,14 +20,12 @@ import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.io.Closeable import java.io.Closeable
import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
@ -37,7 +33,6 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail
class ClientRPCInfrastructureTests { class ClientRPCInfrastructureTests {
// TODO: Test that timeouts work // TODO: Test that timeouts work
@ -47,7 +42,7 @@ class ClientRPCInfrastructureTests {
lateinit var clientSession: ClientSession lateinit var clientSession: ClientSession
lateinit var producer: ClientProducer lateinit var producer: ClientProducer
lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
var proxy: TestOps? = null lateinit var proxy: TestOps
private val authenticatedUser = User("test", "password", permissions = setOf()) private val authenticatedUser = User("test", "password", permissions = setOf())
@ -94,16 +89,10 @@ class ClientRPCInfrastructureTests {
clientSession.start() clientSession.start()
LogHelper.setLevel("+net.corda.rpc") LogHelper.setLevel("+net.corda.rpc")
}
private fun createProxyUsingReplyTo(username: String, timeout: Duration? = null): TestOps { proxy = CordaRPCClientImpl(clientSession, ReentrantLock(), authenticatedUser.username).proxyFor(TestOps::class.java)
val proxy = CordaRPCClientImpl(clientSession, ReentrantLock(), username).proxyFor(TestOps::class.java, timeout = timeout)
this.proxy = proxy
return proxy
} }
private fun createProxyUsingAuthenticatedReplyTo() = createProxyUsingReplyTo(authenticatedUser.username)
@After @After
fun shutdown() { fun shutdown() {
(proxy as Closeable?)?.close() (proxy as Closeable?)?.close()
@ -156,7 +145,6 @@ class ClientRPCInfrastructureTests {
@Test @Test
fun `simple RPCs`() { fun `simple RPCs`() {
val proxy = createProxyUsingAuthenticatedReplyTo()
// Does nothing, doesn't throw. // Does nothing, doesn't throw.
proxy.void() proxy.void()
@ -169,7 +157,6 @@ class ClientRPCInfrastructureTests {
@Test @Test
fun `simple observable`() { fun `simple observable`() {
val proxy = createProxyUsingAuthenticatedReplyTo()
// This tests that the observations are transmitted correctly, also completion is transmitted. // This tests that the observations are transmitted correctly, also completion is transmitted.
val observations = proxy.makeObservable().toBlocking().toIterable().toList() val observations = proxy.makeObservable().toBlocking().toIterable().toList()
assertEquals(listOf(1, 2, 3, 4), observations) assertEquals(listOf(1, 2, 3, 4), observations)
@ -177,7 +164,6 @@ class ClientRPCInfrastructureTests {
@Test @Test
fun `complex observables`() { fun `complex observables`() {
val proxy = createProxyUsingAuthenticatedReplyTo()
// This checks that we can return an object graph with complex usage of observables, like an observable // This checks that we can return an object graph with complex usage of observables, like an observable
// that emits objects that contain more observables. // that emits objects that contain more observables.
val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>() val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>()
@ -224,31 +210,12 @@ class ClientRPCInfrastructureTests {
@Test @Test
fun versioning() { fun versioning() {
val proxy = createProxyUsingAuthenticatedReplyTo()
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() } assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
} }
@Test @Test
fun `authenticated user is available to RPC`() { fun `authenticated user is available to RPC`() {
val proxy = createProxyUsingAuthenticatedReplyTo()
assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username) assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username)
} }
@Test
fun `using another username for the reply-to`() {
assertThatExceptionOfType(RPCException.DeadlineExceeded::class.java).isThrownBy {
val proxy = createProxyUsingReplyTo(random63BitValue().toString(), timeout = 300.millis)
proxy.void()
fail("RPC successfully returned using someone else's username for the reply-to")
}
}
@Test
fun `using another username for the reply-to, which contains our username as a prefix`() {
assertThatExceptionOfType(RPCException.DeadlineExceeded::class.java).isThrownBy {
val proxy = createProxyUsingReplyTo("${authenticatedUser.username}extra", timeout = 300.millis)
proxy.void()
fail("RPC successfully returned using someone else's username for the reply-to")
}
}
} }

View File

@ -107,11 +107,11 @@ object X509Utilities {
} }
/** /**
* Helper method to create Subject field contents * Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
*/ */
fun getDevX509Name(domain: String): X500Name { fun getDevX509Name(commonName: String): X500Name {
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
nameBuilder.addRDN(BCStyle.CN, domain) nameBuilder.addRDN(BCStyle.CN, commonName)
nameBuilder.addRDN(BCStyle.O, "R3") nameBuilder.addRDN(BCStyle.O, "R3")
nameBuilder.addRDN(BCStyle.OU, "corda") nameBuilder.addRDN(BCStyle.OU, "corda")
nameBuilder.addRDN(BCStyle.L, "London") nameBuilder.addRDN(BCStyle.L, "London")
@ -574,18 +574,21 @@ object X509Utilities {
storePassword: String, storePassword: String,
keyPassword: String, keyPassword: String,
caKeyStore: KeyStore, caKeyStore: KeyStore,
caKeyPassword: String): KeyStore { caKeyPassword: String,
val rootCA = loadCertificateAndKey(caKeyStore, commonName: String): KeyStore {
val rootCA = X509Utilities.loadCertificateAndKey(
caKeyStore,
caKeyPassword, caKeyPassword,
CORDA_ROOT_CA_PRIVATE_KEY) CORDA_ROOT_CA_PRIVATE_KEY)
val intermediateCA = loadCertificateAndKey(caKeyStore, val intermediateCA = X509Utilities.loadCertificateAndKey(
caKeyStore,
caKeyPassword, caKeyPassword,
CORDA_INTERMEDIATE_CA_PRIVATE_KEY) CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
val serverKey = generateECDSAKeyPairForSSL() val serverKey = generateECDSAKeyPairForSSL()
val host = InetAddress.getLocalHost() val host = InetAddress.getLocalHost()
val subject = getDevX509Name(host.canonicalHostName) val serverCert = createServerCert(
val serverCert = createServerCert(subject, getDevX509Name(commonName),
serverKey.public, serverKey.public,
intermediateCA, intermediateCA,
if (host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName), if (host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
@ -594,7 +597,8 @@ object X509Utilities {
val keyPass = keyPassword.toCharArray() val keyPass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
keyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY, keyStore.addOrReplaceKey(
CORDA_CLIENT_CA_PRIVATE_KEY,
serverKey.private, serverKey.private,
keyPass, keyPass,
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate)) arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))

View File

@ -7,6 +7,8 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID
import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.serialization.DeserializeAsKotlinObjectDef
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -37,7 +39,7 @@ interface MessagingService {
* @param sessionID identifier for the session the message is part of. For services listening before * @param sessionID identifier for the session the message is part of. For services listening before
* a session is established, use [DEFAULT_SESSION_ID]. * a session is established, use [DEFAULT_SESSION_ID].
*/ */
fun addMessageHandler(topic: String = "", sessionID: Long = DEFAULT_SESSION_ID, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration fun addMessageHandler(topic: String = "", sessionID: Long = DEFAULT_SESSION_ID, callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
/** /**
* The provided function will be invoked for each received message whose topic and session matches. The callback * The provided function will be invoked for each received message whose topic and session matches. The callback
@ -49,7 +51,7 @@ interface MessagingService {
* *
* @param topicSession identifier for the topic and session to listen for messages arriving on. * @param topicSession identifier for the topic and session to listen for messages arriving on.
*/ */
fun addMessageHandler(topicSession: TopicSession, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration fun addMessageHandler(topicSession: TopicSession, callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
/** /**
* Removes a handler given the object returned from [addMessageHandler]. The callback will no longer be invoked once * Removes a handler given the object returned from [addMessageHandler]. The callback will no longer be invoked once
@ -104,7 +106,7 @@ fun MessagingService.createMessage(topic: String, sessionID: Long = DEFAULT_SESS
* @param sessionID identifier for the session the message is part of. For services listening before * @param sessionID identifier for the session the message is part of. For services listening before
* a session is established, use [DEFAULT_SESSION_ID]. * a session is established, use [DEFAULT_SESSION_ID].
*/ */
fun MessagingService.runOnNextMessage(topic: String, sessionID: Long, callback: (Message) -> Unit) fun MessagingService.runOnNextMessage(topic: String, sessionID: Long, callback: (ReceivedMessage) -> Unit)
= runOnNextMessage(TopicSession(topic, sessionID), callback) = runOnNextMessage(TopicSession(topic, sessionID), callback)
/** /**
@ -114,7 +116,7 @@ fun MessagingService.runOnNextMessage(topic: String, sessionID: Long, callback:
* *
* @param topicSession identifier for the topic and session to listen for messages arriving on. * @param topicSession identifier for the topic and session to listen for messages arriving on.
*/ */
inline fun MessagingService.runOnNextMessage(topicSession: TopicSession, crossinline callback: (Message) -> Unit) { inline fun MessagingService.runOnNextMessage(topicSession: TopicSession, crossinline callback: (ReceivedMessage) -> Unit) {
val consumed = AtomicBoolean() val consumed = AtomicBoolean()
addMessageHandler(topicSession) { msg, reg -> addMessageHandler(topicSession) { msg, reg ->
removeMessageHandler(reg) removeMessageHandler(reg)
@ -155,12 +157,7 @@ interface MessageHandlerRegistration
* a session is established, use [DEFAULT_SESSION_ID]. * a session is established, use [DEFAULT_SESSION_ID].
*/ */
data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) { data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) {
companion object {
val Blank = TopicSession("", DEFAULT_SESSION_ID)
}
fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID
override fun toString(): String = "$topic.$sessionID" override fun toString(): String = "$topic.$sessionID"
} }
@ -181,6 +178,15 @@ interface Message {
val uniqueMessageId: UUID val uniqueMessageId: UUID
} }
// TODO Have ReceivedMessage point to the TLS certificate of the peer, and [peer] would simply be the subject DN of that.
// The certificate would need to be serialised into the message header or just its fingerprint and then download it via RPC,
// or something like that.
interface ReceivedMessage : Message {
/** The authenticated sender. */
val peer: X500Name
val peerLegalName: String get() = peer.getRDNs(BCStyle.CN).first().first.value.toString()
}
/** A singleton that's useful for validating topic strings */ /** A singleton that's useful for validating topic strings */
object TopicStringValidator { object TopicStringValidator {
private val regex = "[a-zA-Z0-9.]+".toPattern() private val regex = "[a-zA-Z0-9.]+".toPattern()

View File

@ -49,8 +49,7 @@ object NotaryFlow {
throw NotaryException(NotaryError.SignaturesMissing(ex.missing)) throw NotaryException(NotaryError.SignaturesMissing(ex.missing))
} }
val request = SignRequest(stx, serviceHub.myInfo.legalIdentity) val response = sendAndReceive<Result>(notaryParty, SignRequest(stx))
val response = sendAndReceive<Result>(notaryParty, request)
return validateResponse(response) return validateResponse(response)
} }
@ -95,14 +94,13 @@ object NotaryFlow {
@Suspendable @Suspendable
override fun call() { override fun call() {
val (stx, reqIdentity) = receive<SignRequest>(otherSide).unwrap { it } val stx = receive<SignRequest>(otherSide).unwrap { it.tx }
val wtx = stx.tx val wtx = stx.tx
val result = try { val result = try {
validateTimestamp(wtx) validateTimestamp(wtx)
beforeCommit(stx, reqIdentity) beforeCommit(stx)
commitInputStates(wtx, reqIdentity) commitInputStates(wtx)
val sig = sign(stx.id.bytes) val sig = sign(stx.id.bytes)
Result.Success(sig) Result.Success(sig)
} catch(e: NotaryException) { } catch(e: NotaryException) {
@ -127,12 +125,12 @@ object NotaryFlow {
* undo the commit of the input states (the exact mechanism still needs to be worked out). * undo the commit of the input states (the exact mechanism still needs to be worked out).
*/ */
@Suspendable @Suspendable
open fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) { open fun beforeCommit(stx: SignedTransaction) {
} }
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) { private fun commitInputStates(tx: WireTransaction) {
try { try {
uniquenessProvider.commit(tx.inputs, tx.id, reqIdentity) uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
} catch (e: UniquenessException) { } catch (e: UniquenessException) {
val conflictData = e.error.serialize() val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData.bytes)) val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
@ -146,8 +144,7 @@ object NotaryFlow {
} }
} }
/** TODO: The caller must authenticate instead of just specifying its identity */ data class SignRequest(val tx: SignedTransaction)
data class SignRequest(val tx: SignedTransaction, val callerIdentity: Party)
sealed class Result { sealed class Result {
class Error(val error: NotaryError) : Result() class Error(val error: NotaryError) : Result()

View File

@ -21,11 +21,11 @@ class ValidatingNotaryFlow(otherSide: Party,
NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) { NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
@Suspendable @Suspendable
override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) { override fun beforeCommit(stx: SignedTransaction) {
try { try {
checkSignatures(stx) checkSignatures(stx)
val wtx = stx.tx val wtx = stx.tx
resolveTransaction(reqIdentity, wtx) resolveTransaction(wtx)
wtx.toLedgerTransaction(serviceHub).verify() wtx.toLedgerTransaction(serviceHub).verify()
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
@ -45,7 +45,7 @@ class ValidatingNotaryFlow(otherSide: Party,
} }
@Suspendable @Suspendable
private fun resolveTransaction(reqIdentity: Party, wtx: WireTransaction) { private fun resolveTransaction(wtx: WireTransaction) {
subFlow(ResolveTransactionsFlow(wtx, reqIdentity)) subFlow(ResolveTransactionsFlow(wtx, otherSide))
} }
} }

View File

@ -145,7 +145,7 @@ class X509UtilitiesTest {
val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
// Generate server cert and private key and populate another keystore suitable for SSL // Generate server cert and private key and populate another keystore suitable for SSL
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass") X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", "Mega Corp.")
// Load back server certificate // Load back server certificate
val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
@ -153,9 +153,8 @@ class X509UtilitiesTest {
serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.checkValidity(Date())
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
val host = InetAddress.getLocalHost()
assertTrue { serverCertAndKey.certificate.subjectDN.name.contains("CN=" + host.canonicalHostName) } assertTrue { serverCertAndKey.certificate.subjectDN.name.contains("CN=Mega Corp.") }
// Now sign something with private key and verify against certificate public key // Now sign something with private key and verify against certificate public key
val testData = "123456".toByteArray() val testData = "123456".toByteArray()
@ -183,7 +182,7 @@ class X509UtilitiesTest {
"trustpass") "trustpass")
// Generate server cert and private key and populate another keystore suitable for SSL // Generate server cert and private key and populate another keystore suitable for SSL
val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass") val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", "Mega Corp.")
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass") val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
val context = SSLContext.getInstance("TLS") val context = SSLContext.getInstance("TLS")
@ -257,8 +256,7 @@ class X509UtilitiesTest {
val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal
val x500name = X500Name(peerX500Principal.name) val x500name = X500Name(peerX500Principal.name)
val cn = x500name.getRDNs(BCStyle.CN).first().first.value.toString() val cn = x500name.getRDNs(BCStyle.CN).first().first.value.toString()
val hostname = InetAddress.getLocalHost().canonicalHostName assertEquals("Mega Corp.", cn)
assertEquals(hostname, cn)
val output = DataOutputStream(clientSocket.outputStream) val output = DataOutputStream(clientSocket.outputStream)

View File

@ -17,15 +17,6 @@ messaging subsystem directly. Instead you will build on top of the :doc:`flow fr
which adds a layer on top of raw messaging to manage multi-step flows and let you think in terms of identities which adds a layer on top of raw messaging to manage multi-step flows and let you think in terms of identities
rather than specific network endpoints. rather than specific network endpoints.
Messaging types
---------------
Every ``Message`` object has an associated *topic* and may have a *session ID*. These are wrapped in a ``TopicSession``.
An implementation of ``MessagingService`` can be used to create messages and send them. You can get access to the
messaging service via the ``ServiceHub`` object that is provided to your app. Endpoints on the network are
identified at the lowest level using ``SingleMessageRecipient`` which may be e.g. an IP address, or in future
versions perhaps a routing path through the network.
.. _network-map-service: .. _network-map-service:
Network Map Service Network Map Service
@ -48,4 +39,72 @@ The network map currently supports:
* Looking up node for a party * Looking up node for a party
* Suggesting a node providing a specific service, based on suitability for a contract and parties, for example suggesting * Suggesting a node providing a specific service, based on suitability for a contract and parties, for example suggesting
an appropriate interest rates oracle for a interest rate swap contract. Currently no recommendation logic is in place. an appropriate interest rates oracle for a interest rate swap contract. Currently no recommendation logic is in place.
The code simply picks the first registered node that supports the required service. The code simply picks the first registered node that supports the required service.
Message queues
--------------
The node makes use of various queues for its operation. The more important ones are described below. Others are used
for maintenance and other minor purposes.
:``p2p.inbound``
The node listens for messages sent from other peer nodes on this queue. Only clients who are authenticated to be
nodes on the same network are given permission to send. Messages which are routed internally are also sent to this
queue (e.g. two flows on the same node communicating with each other).
:``internal.peers.$identity``
These are a set of private queues only available to the node which it uses to route messages destined to other peers.
The queue name ends in the base 58 encoding of the peer's identity key. There is at most one queue per peer. The broker
creates a bridge from this queue to the peer's ``p2p.inbound`` queue, using the network map service to lookup the
peer's network address.
:``internal.networkmap``
This is another private queue just for the node which functions in a similar manner to the ``p2p.peers.*`` queues
except this is used to form a connection to the network map node. The node running the network map service is treated
differently as it provides information about the rest of the network.
:``rpc.requests``
RPC clients send their requests here, and it's only open for sending by clients authenticated as RPC users.
:``clients.$user.rpc.$random``
RPC clients are given permission to create a temporary queue incorporating their username (``$user``) and sole
permission to receive messages from it. RPC requests are required to include a random number (``$random``) from
which the node is able to construct the queue the user is listening on and send the response to that. This mechanism
prevents other users from being able listen in on the responses.
Security
--------
Clients attempting to connect to the node's broker fall in one of four groups:
# Anyone connecting with the username ``SystemUsers/Node`` is treated as the node hosting the broker, or a logical
component of the node. The TLS certificate they provide must match the one broker has for the node. If that's the case
they are given full access to all valid queues, otherwise they are rejected.
# Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their
TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA
implies we've been let in by the same doorman. If they are part of the same network then they are only given permission
to send to our ``p2p.inbound`` queue, otherwise they are rejected.
# Every other username is treated as a RPC user and authenticated against the node's list of valid RPC users. If that
is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected.
# Clients connecting without a username and password are rejected.
Artemis provides a feature of annotating each received message with the validated user. This allows the node's messaging
service to provide authenticated messages to the rest of the system. For the first two client types described above the
validated user is the X.500 subject DN of the client TLS certificate and we assume the common name is the legal name of
the peer. This allows the flow framework to authentically determine the ``Party`` initiating a new flow. For RPC clients
the validated user is the username itself and the RPC framework uses this to determine what permissions the user has.
.. note:: ``Party`` lookup is currently done by the legal name which isn't guaranteed to be unique. A future version will
use the full X.500 name as it can provide additional structures for uniqueness.
Messaging types
---------------
Every ``Message`` object has an associated *topic* and may have a *session ID*. These are wrapped in a ``TopicSession``.
An implementation of ``MessagingService`` can be used to create messages and send them. You can get access to the
messaging service via the ``ServiceHub`` object that is provided to your app. Endpoints on the network are
identified at the lowest level using ``SingleMessageRecipient`` which may be e.g. an IP address, or in future
versions perhaps a routing path through the network.

View File

@ -20,13 +20,8 @@ Setting up your own network
Certificates Certificates
------------ ------------
If two nodes are to communicate successfully then both need to have All nodes belonging to the same Corda network must have the same root CA. For testing purposes you can
each other's root certificate in their truststores. The simplest way use ``certSigningRequestUtility.jar`` to generate a node certificate with a fixed test root:
to achieve this is to have all nodes sign off of a single root.
Later R3 will provide this root for production use, but for testing you
can use ``certSigningRequestUtility.jar`` to generate a node
certificate with a fixed test root:
.. sourcecode:: bash .. sourcecode:: bash

View File

@ -62,6 +62,7 @@ sourceSets {
dependencies { dependencies {
compile project(':finance') compile project(':finance')
testCompile project(':test-utils') testCompile project(':test-utils')
testCompile project(':client')
compile "com.google.code.findbugs:jsr305:3.0.1" compile "com.google.code.findbugs:jsr305:3.0.1"

View File

@ -0,0 +1,230 @@
package net.corda.services.messaging
import co.paralleluniverse.fibers.Suspendable
import com.google.common.net.HostAndPort
import net.corda.client.impl.CordaRPCClientImpl
import net.corda.core.crypto.Party
import net.corda.core.crypto.composite
import net.corda.core.crypto.generateKeyPair
import net.corda.core.flows.FlowLogic
import net.corda.core.getOrThrow
import net.corda.core.random63BitValue
import net.corda.core.seconds
import net.corda.node.internal.Node
import net.corda.node.services.User
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_ADDRESS
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
import net.corda.node.services.messaging.CordaRPCOps
import net.corda.node.services.messaging.NodeMessagingClient.Companion.RPC_QUEUE_REMOVALS_QUEUE
import net.corda.testing.messaging.SimpleMQClient
import net.corda.testing.node.NodeBasedTest
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.apache.activemq.artemis.api.core.SimpleString
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.*
import java.util.concurrent.locks.ReentrantLock
/**
* Runs a series of MQ-related attacks against a node. Subclasses need to call [startAttacker] to connect
* the attacker to [alice].
*/
abstract class MQSecurityTest : NodeBasedTest() {
val rpcUser = User("user1", "pass", permissions = emptySet())
lateinit var alice: Node
lateinit var attacker: SimpleMQClient
private val clients = ArrayList<SimpleMQClient>()
@Before
fun start() {
alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser)
attacker = SimpleMQClient(alice.configuration.artemisAddress)
startAttacker(attacker)
}
open val extraRPCUsers: List<User> get() = emptyList()
abstract fun startAttacker(attacker: SimpleMQClient)
@After
fun stopClients() {
clients.forEach { it.stop() }
}
@Test
fun `consume message from P2P queue`() {
assertConsumeAttackFails(P2P_QUEUE)
}
@Test
fun `consume message from peer queue`() {
val bobParty = startBobAndCommunicateWithAlice()
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
}
@Test
fun `send message to peer address`() {
val bobParty = startBobAndCommunicateWithAlice()
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
}
@Test
fun `create queue for unknown peer`() {
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.composite.toBase58String()}"
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = true)
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = false)
assertTempQueueCreationAttackFails(invalidPeerQueue)
}
@Test
fun `consume message from network map queue`() {
assertConsumeAttackFails(NETWORK_MAP_ADDRESS.toString())
}
@Test
fun `send message to network map address`() {
assertSendAttackFails(NETWORK_MAP_ADDRESS.toString())
}
@Test
fun `consume message from RPC requests queue`() {
assertConsumeAttackFails(RPC_REQUESTS_QUEUE)
}
@Test
fun `consume message from logged in user's RPC queue`() {
val user1Queue = loginToRPCAndGetClientQueue()
assertConsumeAttackFails(user1Queue)
}
@Test
fun `send message on logged in user's RPC address`() {
val user1Queue = loginToRPCAndGetClientQueue()
assertSendAttackFails(user1Queue)
}
@Test
fun `create queue for valid RPC user`() {
val user1Queue = "$CLIENTS_PREFIX${rpcUser.username}.rpc.${random63BitValue()}"
assertTempQueueCreationAttackFails(user1Queue)
}
@Test
fun `create queue for invalid RPC user`() {
val invalidRPCQueue = "$CLIENTS_PREFIX${random63BitValue()}.rpc.${random63BitValue()}"
assertTempQueueCreationAttackFails(invalidRPCQueue)
}
@Test
fun `consume message from RPC queue removals queue`() {
assertConsumeAttackFails(RPC_QUEUE_REMOVALS_QUEUE)
}
@Test
fun `send message to notifications address`() {
assertSendAttackFails(NOTIFICATIONS_ADDRESS)
}
@Test
fun `create random queue`() {
val randomQueue = random63BitValue().toString()
assertNonTempQueueCreationAttackFails(randomQueue, durable = false)
assertNonTempQueueCreationAttackFails(randomQueue, durable = true)
assertTempQueueCreationAttackFails(randomQueue)
}
fun clientTo(target: HostAndPort): SimpleMQClient {
val client = SimpleMQClient(target)
clients += client
return client
}
fun loginToRPC(target: HostAndPort, rpcUser: User): SimpleMQClient {
val client = clientTo(target)
client.loginToRPC(rpcUser)
return client
}
fun SimpleMQClient.loginToRPC(rpcUser: User): CordaRPCOps {
start(rpcUser.username, rpcUser.password)
val clientImpl = CordaRPCClientImpl(session, ReentrantLock(), rpcUser.username)
return clientImpl.proxyFor(CordaRPCOps::class.java, timeout = 1.seconds)
}
fun loginToRPCAndGetClientQueue(): String {
val rpcClient = loginToRPC(alice.configuration.artemisAddress, rpcUser)
val clientQueueQuery = SimpleString("$CLIENTS_PREFIX${rpcUser.username}.rpc.*")
return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString()
}
fun assertTempQueueCreationAttackFails(queue: String) {
assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") {
attacker.session.createTemporaryQueue(queue, queue)
}
// Double-check
assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy {
attacker.session.createConsumer(queue)
}
}
fun assertNonTempQueueCreationAttackFails(queue: String, durable: Boolean) {
val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE"
assertAttackFails(queue, permission) {
attacker.session.createQueue(queue, queue, durable)
}
// Double-check
assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy {
attacker.session.createConsumer(queue)
}
}
fun assertSendAttackFails(address: String) {
val message = attacker.createMessage()
assertAttackFails(address, "SEND") {
attacker.producer.send(address, message)
}
// TODO Make sure no actual message is received
}
fun assertConsumeAttackFails(queue: String) {
assertAttackFails(queue, "CONSUME") {
attacker.session.createConsumer(queue)
}
assertAttackFails(queue, "BROWSE") {
attacker.session.createConsumer(queue, true)
}
}
fun assertAttackFails(queue: String, permission: String, attack: () -> Unit) {
assertThatExceptionOfType(ActiveMQSecurityException::class.java)
.isThrownBy(attack)
.withMessageContaining(queue)
.withMessageContaining(permission)
}
private fun startBobAndCommunicateWithAlice(): Party {
val bob = startNode("Bob")
bob.services.registerFlowInitiator(SendFlow::class, ::ReceiveFlow)
val bobParty = bob.info.legalIdentity
// Perform a protocol exchange to force the peer queue to be created
alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow()
return bobParty
}
private class SendFlow(val otherParty: Party, val payload: Any) : FlowLogic<Unit>() {
@Suspendable
override fun call() = send(otherParty, payload)
}
private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() {
@Suspendable
override fun call() = receive<Any>(otherParty).unwrap { it }
}
}

View File

@ -0,0 +1,53 @@
package net.corda.services.messaging
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
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.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
/**
* Runs the security tests with the attacker pretending to be a node on the network.
*/
class P2PSecurityTest : MQSecurityTest() {
override fun startAttacker(attacker: SimpleMQClient) {
attacker.start(PEER_USER, PEER_USER) // Login as a peer
}
@Test
fun `send message to RPC requests address`() {
assertSendAttackFails(RPC_REQUESTS_QUEUE)
}
@Test
fun `only the node running the broker can login using the special node user`() {
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(NODE_USER, NODE_USER)
}
attacker.stop()
}
@Test
fun `login as the default cluster user`() {
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQClusterSecurityException::class.java).isThrownBy {
attacker.start(ActiveMQDefaultConfiguration.getDefaultClusterUser(), ActiveMQDefaultConfiguration.getDefaultClusterPassword())
}
attacker.stop()
}
@Test
fun `login without a username and password`() {
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start()
}
attacker.stop()
}
}

View File

@ -0,0 +1,15 @@
package net.corda.services.messaging
import net.corda.node.services.User
import net.corda.testing.messaging.SimpleMQClient
/**
* Runs the security tests with the attacker being a valid RPC user of Alice.
*/
class RPCSecurityTest : MQSecurityTest() {
override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet()))
override fun startAttacker(attacker: SimpleMQClient) {
attacker.loginToRPC(extraRPCUsers[0])
}
}

View File

@ -15,6 +15,7 @@ import net.corda.node.services.RPCUserService
import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.services.api.MessagingServiceInternal import net.corda.node.services.api.MessagingServiceInternal
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.messaging.RPCOps import net.corda.node.services.messaging.RPCOps
@ -118,15 +119,15 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
private lateinit var userService: RPCUserService private lateinit var userService: RPCUserService
override fun makeMessagingService(): MessagingServiceInternal { override fun makeMessagingService(): MessagingServiceInternal {
val legalIdentity = obtainLegalIdentity()
val myIdentityOrNullIfNetworkMapService = if (networkMapService != null) legalIdentity.owningKey else null
userService = RPCUserServiceImpl(configuration) userService = RPCUserServiceImpl(configuration)
val serverAddr = with(configuration) { val serverAddr = with(configuration) {
messagingServerAddress ?: { messagingServerAddress ?: {
messageBroker = ArtemisMessagingServer(this, artemisAddress, myIdentityOrNullIfNetworkMapService, services.networkMapCache, userService) messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService)
artemisAddress artemisAddress
}() }()
} }
val legalIdentity = obtainLegalIdentity()
val myIdentityOrNullIfNetworkMapService = if (networkMapService != null) legalIdentity.owningKey else null
return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread, database, networkMapRegistrationFuture) return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread, database, networkMapRegistrationFuture)
} }
@ -135,12 +136,13 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
messageBroker?.apply { messageBroker?.apply {
runOnStop += Runnable { messageBroker?.stop() } runOnStop += Runnable { messageBroker?.stop() }
start() start()
bridgeToNetworkMapService(networkMapService) if (networkMapService is NetworkMapAddress) {
bridgeToNetworkMapService(networkMapService)
}
} }
// Start up the MQ client. // Start up the MQ client.
val net = net as NodeMessagingClient val net = net as NodeMessagingClient
net.configureWithDevSSLCertificate() // TODO: Client might need a separate certificate
net.start(rpcOps, userService) net.start(rpcOps, userService)
} }
@ -151,7 +153,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
// Export JMX monitoring statistics and data over REST/JSON. // Export JMX monitoring statistics and data over REST/JSON.
if (configuration.exportJMXto.split(',').contains("http")) { if (configuration.exportJMXto.split(',').contains("http")) {
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator")) val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
val warpath = classpath.firstOrNull() { it.contains("jolokia-agent-war-2") && it.endsWith(".war") } val warpath = classpath.firstOrNull { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
if (warpath != null) { if (warpath != null) {
handlerCollection.addHandler(WebAppContext().apply { handlerCollection.addHandler(WebAppContext().apply {
// Find the jolokia WAR file on the classpath. // Find the jolokia WAR file on the classpath.
@ -174,7 +176,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.outputBufferSize = 32768
httpsConfiguration.addCustomizer(SecureRequestCustomizer()) httpsConfiguration.addCustomizer(SecureRequestCustomizer())
val sslContextFactory = SslContextFactory() val sslContextFactory = SslContextFactory()
sslContextFactory.setKeyStorePath(configuration.keyStorePath.toString()) sslContextFactory.keyStorePath = configuration.keyStorePath.toString()
sslContextFactory.setKeyStorePassword(configuration.keyStorePassword) sslContextFactory.setKeyStorePassword(configuration.keyStorePassword)
sslContextFactory.setKeyManagerPassword(configuration.keyStorePassword) sslContextFactory.setKeyManagerPassword(configuration.keyStorePassword)
sslContextFactory.setTrustStorePath(configuration.trustStorePath.toString()) sslContextFactory.setTrustStorePath(configuration.trustStorePath.toString())

View File

@ -100,7 +100,9 @@ inline fun <reified T : Any> Config.getListOrElse(path: String, default: Config.
* Strictly for dev only automatically construct a server certificate/private key signed from * Strictly for dev only automatically construct a server certificate/private key signed from
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path. * the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
*/ */
fun NodeSSLConfiguration.configureWithDevSSLCertificate() { fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
private fun NodeSSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) {
certificatesPath.createDirectories() certificatesPath.createDirectories()
if (!trustStorePath.exists()) { if (!trustStorePath.exists()) {
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStorePath) javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStorePath)
@ -109,7 +111,7 @@ fun NodeSSLConfiguration.configureWithDevSSLCertificate() {
val caKeyStore = X509Utilities.loadKeyStore( val caKeyStore = X509Utilities.loadKeyStore(
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"),
"cordacadevpass") "cordacadevpass")
X509Utilities.createKeystoreForSSL(keyStorePath, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass") X509Utilities.createKeystoreForSSL(keyStorePath, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
} }
} }
@ -120,6 +122,6 @@ fun configureTestSSL(): NodeSSLConfiguration = object : NodeSSLConfiguration {
override val trustStorePassword: String get() = "trustpass" override val trustStorePassword: String get() = "trustpass"
init { init {
configureWithDevSSLCertificate() configureDevKeyAndTrustStores("Mega Corp.")
} }
} }

View File

@ -8,7 +8,6 @@ import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.read import net.corda.core.read
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.config.NodeSSLConfiguration import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
@ -28,12 +27,20 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
System.setProperty("org.jboss.logging.provider", "slf4j") System.setProperty("org.jboss.logging.provider", "slf4j")
} }
const val PEERS_PREFIX = "peers." // System users must contain an invalid RPC username character to prevent any chance of name clash which in this
// case is a forward slash
const val NODE_USER = "SystemUsers/Node"
const val PEER_USER = "SystemUsers/Peer"
const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers."
const val CLIENTS_PREFIX = "clients." const val CLIENTS_PREFIX = "clients."
const val P2P_QUEUE = "p2p.inbound"
const val RPC_REQUESTS_QUEUE = "rpc.requests" const val RPC_REQUESTS_QUEUE = "rpc.requests"
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
@JvmStatic @JvmStatic
protected val NETWORK_MAP_ADDRESS = SimpleString("${PEERS_PREFIX}networkmap") val NETWORK_MAP_ADDRESS = SimpleString("${INTERNAL_PREFIX}networkmap")
/** /**
* Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should * Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should
@ -46,27 +53,6 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
val addr = target as? ArtemisMessagingComponent.ArtemisAddress ?: throw IllegalArgumentException("Not an Artemis address") val addr = target as? ArtemisMessagingComponent.ArtemisAddress ?: throw IllegalArgumentException("Not an Artemis address")
return addr.hostAndPort return addr.hostAndPort
} }
/**
* Assuming the passed in target address is actually an ArtemisAddress will extract the queue name used.
* For now the queue name is the Base58 version of the node's identity.
* This should only be used in the internals of the messaging services to keep addressing opaque for the future.
* N.B. Marked as JvmStatic to allow use in the inherited classes.
*/
@JvmStatic
protected fun toQueueName(target: MessageRecipients): SimpleString {
val addr = target as? ArtemisMessagingComponent.ArtemisAddress ?: throw IllegalArgumentException("Not an Artemis address")
return addr.queueName
}
/**
* Convert the identity, host and port of this node into the appropriate [SingleMessageRecipient].
*
* N.B. Marked as JvmStatic to allow use in the inherited classes.
*/
@JvmStatic
protected fun toMyAddress(myIdentity: CompositeKey?, myHostPort: HostAndPort): SingleMessageRecipient = if (myIdentity != null) NodeAddress(myIdentity, myHostPort) else NetworkMapAddress(myHostPort)
} }
protected interface ArtemisAddress { protected interface ArtemisAddress {
@ -74,7 +60,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
val hostAndPort: HostAndPort val hostAndPort: HostAndPort
} }
protected data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress { data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
override val queueName: SimpleString get() = NETWORK_MAP_ADDRESS override val queueName: SimpleString get() = NETWORK_MAP_ADDRESS
} }
@ -84,18 +70,13 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
* For instance it may contain onion routing data. * For instance it may contain onion routing data.
*/ */
data class NodeAddress(val identity: CompositeKey, override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress { data class NodeAddress(val identity: CompositeKey, override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
override val queueName: SimpleString by lazy { SimpleString(PEERS_PREFIX + identity.toBase58String()) } override val queueName: SimpleString = SimpleString("$PEERS_PREFIX${identity.toBase58String()}")
override fun toString(): String = "${javaClass.simpleName}(identity = $queueName, $hostAndPort)" override fun toString(): String = "${javaClass.simpleName}(identity = $queueName, $hostAndPort)"
} }
/** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */ /** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */
abstract val config: NodeSSLConfiguration abstract val config: NodeSSLConfiguration
protected fun parseKeyFromQueueName(name: String): CompositeKey {
require(name.startsWith(PEERS_PREFIX))
return CompositeKey.parseFromBase58(name.substring(PEERS_PREFIX.length))
}
protected enum class ConnectionDirection { INBOUND, OUTBOUND } protected enum class ConnectionDirection { INBOUND, OUTBOUND }
// Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher. // Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher.
@ -135,7 +116,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
mapOf( mapOf(
// Basic TCP target details // Basic TCP target details
TransportConstants.HOST_PROP_NAME to host, TransportConstants.HOST_PROP_NAME to host,
TransportConstants.PORT_PROP_NAME to port.toInt(), TransportConstants.PORT_PROP_NAME to port,
// Turn on AMQP support, which needs the protocol jar on the classpath. // Turn on AMQP support, which needs the protocol jar on the classpath.
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
@ -159,10 +140,6 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
) )
} }
fun configureWithDevSSLCertificate() {
config.configureWithDevSSLCertificate()
}
protected fun Path.expectedOnDefaultFileSystem() { protected fun Path.expectedOnDefaultFileSystem() {
require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" } require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
} }

View File

@ -4,14 +4,23 @@ import com.google.common.net.HostAndPort
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.crypto.AddressFormatException import net.corda.core.crypto.AddressFormatException
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.core.crypto.newSecureRandom
import net.corda.core.div import net.corda.core.div
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChangeType
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo import net.corda.node.printBasicNodeInfo
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_USER import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.INBOUND
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.OUTBOUND
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.PEER_ROLE
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.RPC_ROLE
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.Configuration
@ -21,11 +30,14 @@ import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager 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.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import rx.Subscription import rx.Subscription
import java.io.IOException import java.io.IOException
import java.math.BigInteger
import java.security.Principal import java.security.Principal
import java.security.PublicKey
import java.util.* import java.util.*
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.Subject import javax.security.auth.Subject
@ -55,10 +67,8 @@ import javax.security.auth.spi.LoginModule
@ThreadSafe @ThreadSafe
class ArtemisMessagingServer(override val config: NodeConfiguration, class ArtemisMessagingServer(override val config: NodeConfiguration,
val myHostPort: HostAndPort, val myHostPort: HostAndPort,
val myIdentity: CompositeKey?,
val networkMapCache: NetworkMapCache, val networkMapCache: NetworkMapCache,
val userService: RPCUserService) : ArtemisMessagingComponent() { val userService: RPCUserService) : ArtemisMessagingComponent() {
companion object { companion object {
private val log = loggerFor<ArtemisMessagingServer>() private val log = loggerFor<ArtemisMessagingServer>()
} }
@ -70,7 +80,6 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
private val mutex = ThreadBox(InnerState()) private val mutex = ThreadBox(InnerState())
private lateinit var activeMQServer: ActiveMQServer private lateinit var activeMQServer: ActiveMQServer
private var networkChangeHandle: Subscription? = null private var networkChangeHandle: Subscription? = null
private val myQueueName = toQueueName(toMyAddress(myIdentity, myHostPort))
init { init {
config.basedir.expectedOnDefaultFileSystem() config.basedir.expectedOnDefaultFileSystem()
@ -79,7 +88,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
fun start() = mutex.locked { fun start() = mutex.locked {
if (!running) { if (!running) {
configureAndStartServer() configureAndStartServer()
networkChangeHandle = networkMapCache.changed.subscribe { onNetworkChange(it) } networkChangeHandle = networkMapCache.changed.subscribe { destroyPossibleStaleBridge(it) }
running = true running = true
} }
} }
@ -91,50 +100,29 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
running = false running = false
} }
fun bridgeToNetworkMapService(networkMapService: SingleMessageRecipient?) { fun bridgeToNetworkMapService(networkMapService: NetworkMapAddress) {
if ((networkMapService != null) && (networkMapService is NetworkMapAddress)) { val query = activeMQServer.queueQuery(NETWORK_MAP_ADDRESS)
val query = activeMQServer.queueQuery(NETWORK_MAP_ADDRESS) if (!query.isExists) {
if (!query.isExists) { activeMQServer.createQueue(NETWORK_MAP_ADDRESS, NETWORK_MAP_ADDRESS, null, true, false)
activeMQServer.createQueue(NETWORK_MAP_ADDRESS, NETWORK_MAP_ADDRESS, null, true, false)
}
maybeDeployBridgeForAddress(NETWORK_MAP_ADDRESS, networkMapService)
} }
maybeDeployBridgeForAddress(networkMapService)
} }
private fun onNetworkChange(change: NetworkMapCache.MapChange) { private fun destroyPossibleStaleBridge(change: NetworkMapCache.MapChange) {
val address = change.node.address fun removePreviousBridge() {
if (address is ArtemisMessagingComponent.ArtemisAddress) { (change.prevNodeInfo?.address as? ArtemisAddress)?.let {
val queueName = address.queueName maybeDestroyBridge(it.queueName)
when (change.type) { }
NetworkMapCache.MapChangeType.Added -> { }
val query = activeMQServer.queueQuery(queueName)
if (query.isExists) {
// Queue exists so now wire up bridge
maybeDeployBridgeForAddress(queueName, change.node.address)
}
}
NetworkMapCache.MapChangeType.Modified -> { if (change.type == MapChangeType.Modified) {
(change.prevNodeInfo?.address as? ArtemisMessagingComponent.ArtemisAddress)?.let { removePreviousBridge()
// remove any previous possibly different bridge } else if (change.type == MapChangeType.Removed) {
maybeDestroyBridge(it.queueName) removePreviousBridge()
} // TODO Fix the network map change data classes so that the remove event doesn't have two NodeInfo fields
val query = activeMQServer.queueQuery(queueName) val address = change.node.address
if (query.isExists) { if (address is ArtemisAddress) {
// Deploy new bridge maybeDestroyBridge(address.queueName)
maybeDeployBridgeForAddress(queueName, change.node.address)
}
}
NetworkMapCache.MapChangeType.Removed -> {
(change.prevNodeInfo?.address as? ArtemisMessagingComponent.ArtemisAddress)?.let {
// Remove old bridge
maybeDestroyBridge(it.queueName)
}
// just in case of NetworkMapCache version issues
maybeDestroyBridge(queueName)
}
} }
} }
} }
@ -142,71 +130,75 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
private fun configureAndStartServer() { private fun configureAndStartServer() {
val config = createArtemisConfig() val config = createArtemisConfig()
val securityManager = createArtemisSecurityManager() val securityManager = createArtemisSecurityManager()
activeMQServer = ActiveMQServerImpl(config, securityManager).apply { activeMQServer = ActiveMQServerImpl(config, securityManager).apply {
// Throw any exceptions which are detected during startup // Throw any exceptions which are detected during startup
registerActivationFailureListener { exception -> throw exception } registerActivationFailureListener { exception -> throw exception }
// Some types of queue might need special preparation on our side, like dialling back or preparing // Some types of queue might need special preparation on our side, like dialling back or preparing
// a lazily initialised subsystem. // a lazily initialised subsystem.
registerPostQueueCreationCallback { queueName -> registerPostQueueCreationCallback { deployBridgeFromNewPeerQueue(it) }
log.debug("Queue created: $queueName") registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
if (queueName.startsWith(PEERS_PREFIX) && queueName != NETWORK_MAP_ADDRESS && queueName != myQueueName) {
try {
val identity = parseKeyFromQueueName(queueName.toString())
val nodeInfo = networkMapCache.getNodeByCompositeKey(identity)
if (nodeInfo != null) {
maybeDeployBridgeForAddress(queueName, nodeInfo.address)
} else {
log.error("Queue created for a peer that we don't know from the network map: $queueName")
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse queue name as Base 58: $queueName")
}
}
}
registerPostQueueDeletionCallback { address, qName ->
if (qName == address)
log.debug("Queue deleted: $qName")
else
log.debug("Queue deleted: $qName for $address")
}
} }
activeMQServer.start() activeMQServer.start()
printBasicNodeInfo("Node listening on address", myHostPort.toString()) printBasicNodeInfo("Node listening on address", myHostPort.toString())
} }
private fun deployBridgeFromNewPeerQueue(queueName: SimpleString) {
log.debug { "Queue created: $queueName" }
if (!queueName.startsWith(PEERS_PREFIX)) return
try {
val identity = CompositeKey.parseFromBase58(queueName.substring(PEERS_PREFIX.length))
val nodeInfo = networkMapCache.getNodeByCompositeKey(identity)
if (nodeInfo != null) {
val address = nodeInfo.address
if (address is NodeAddress) {
maybeDeployBridgeForAddress(address)
} else {
log.error("Don't know how to deal with $address")
}
} else {
log.error("Queue created for a peer that we don't know from the network map: $queueName")
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse queue name as Base 58: $queueName")
}
}
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply { private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {
val artemisDir = config.basedir / "artemis" val artemisDir = config.basedir / "artemis"
bindingsDirectory = (artemisDir / "bindings").toString() bindingsDirectory = (artemisDir / "bindings").toString()
journalDirectory = (artemisDir / "journal").toString() journalDirectory = (artemisDir / "journal").toString()
largeMessagesDirectory = (artemisDir / "largemessages").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString()
acceptorConfigurations = setOf( acceptorConfigurations = setOf(tcpTransport(INBOUND, "0.0.0.0", myHostPort.port))
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", myHostPort.port)
)
// Enable built in message deduplication. Note we still have to do our own as the delayed commits // Enable built in message deduplication. Note we still have to do our own as the delayed commits
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates. // and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
isPersistIDCache = true isPersistIDCache = true
isPopulateValidatedUser = true isPopulateValidatedUser = true
configureQueueSecurity() 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
// password is changed from the default (as warned in the docs). Since we don't need this feature we turn it off
// by having its password be an unknown securely random 128-bit value.
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
configureAddressSecurity()
} }
private fun ConfigurationImpl.configureQueueSecurity() { /**
val nodeRPCSendRole = restrictedRole(NODE_USER, send = true) // The node needs to be able to send responses on the client queues * Authenticated clients connecting to us fall in one of three groups:
* 1. The node hosting us and any of its logically connected components. These are given full access to all valid queues.
* 2. Peers on the same network as us. These are only given permission to send to our P2P inbound queue.
* 3. RPC users. These are only given sufficient access to perform RPC with us.
*/
private fun ConfigurationImpl.configureAddressSecurity() {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
securityRoles[RPC_REQUESTS_QUEUE] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
for ((username) in userService.users) { for ((username) in userService.users) {
// Clients need to be able to consume the responses on their queues, and they're also responsible for creating and destroying them securityRoles["$CLIENTS_PREFIX$username.rpc.*"] = setOf(
val clientRole = restrictedRole(username, consume = true, createNonDurableQueue = true, deleteNonDurableQueue = true) nodeInternalRole,
securityRoles["$CLIENTS_PREFIX$username.rpc.*"] = setOf(nodeRPCSendRole, clientRole) restrictedRole("$CLIENTS_PREFIX$username", consume = true, createNonDurableQueue = true, deleteNonDurableQueue = true))
} }
// TODO Restrict this down to just what the node needs
securityRoles["#"] = setOf(Role(NODE_USER, true, true, true, true, true, true, true, true))
securityRoles[RPC_REQUESTS_QUEUE] = setOf(
restrictedRole(NODE_USER, createNonDurableQueue = true, deleteNonDurableQueue = true),
restrictedRole(RPC_REQUESTS_QUEUE, send = true)) // Clients need to be able to send their requests
} }
private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false, private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false,
@ -217,14 +209,22 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
} }
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
val ourRootCAPublicKey = X509Utilities
.loadCertificateFromKeyStore(config.trustStorePath, config.trustStorePassword, CORDA_ROOT_CA)
.publicKey
val ourPublicKey = X509Utilities
.loadCertificateFromKeyStore(config.keyStorePath, config.keyStorePassword, CORDA_CLIENT_CA)
.publicKey
val securityConfig = object : SecurityConfiguration() { val securityConfig = object : SecurityConfiguration() {
// Override to make it work with our login module // Override to make it work with our login module
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf(RPCUserService::class.java.name to userService) val options = mapOf(
RPCUserService::class.java.name to userService,
CORDA_ROOT_CA to ourRootCAPublicKey,
CORDA_CLIENT_CA to ourPublicKey)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options)) return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
} }
} }
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig) return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
} }
@ -232,39 +232,39 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
private fun addConnector(hostAndPort: HostAndPort) = activeMQServer.configuration.addConnectorConfiguration( private fun addConnector(hostAndPort: HostAndPort) = activeMQServer.configuration.addConnectorConfiguration(
hostAndPort.toString(), hostAndPort.toString(),
tcpTransport( tcpTransport(OUTBOUND, hostAndPort.hostText, hostAndPort.port)
ConnectionDirection.OUTBOUND,
hostAndPort.hostText,
hostAndPort.port
)
) )
private fun bridgeExists(name: SimpleString) = activeMQServer.clusterManager.bridges.containsKey(name.toString()) private fun bridgeExists(name: SimpleString) = activeMQServer.clusterManager.bridges.containsKey(name.toString())
private fun deployBridge(hostAndPort: HostAndPort, name: String) { private fun maybeDeployBridgeForAddress(address: ArtemisAddress) {
activeMQServer.deployBridge(BridgeConfiguration().apply { if (!connectorExists(address.hostAndPort)) {
setName(name) addConnector(address.hostAndPort)
queueName = name }
forwardingAddress = name if (!bridgeExists(address.queueName)) {
staticConnectors = listOf(hostAndPort.toString()) deployBridge(address)
confirmationWindowSize = 100000 // a guess }
isUseDuplicateDetection = true // Enable the bridges automatic deduplication logic
})
} }
/** /**
* For every queue created we need to have a bridge deployed in case the address of the queue * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
* is that of a remote party. * 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 maybeDeployBridgeForAddress(name: SimpleString, nodeInfo: SingleMessageRecipient) { private fun deployBridge(address: ArtemisAddress) {
require(name.startsWith(PEERS_PREFIX)) activeMQServer.deployBridge(BridgeConfiguration().apply {
val hostAndPort = toHostAndPort(nodeInfo) name = address.queueName.toString()
if (hostAndPort == myHostPort) queueName = address.queueName.toString()
return forwardingAddress = P2P_QUEUE
if (!connectorExists(hostAndPort)) staticConnectors = listOf(address.hostAndPort.toString())
addConnector(hostAndPort) confirmationWindowSize = 100000 // a guess
if (!bridgeExists(name)) isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
deployBridge(hostAndPort, name.toString()) // 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 maybeDestroyBridge(name: SimpleString) { private fun maybeDestroyBridge(name: SimpleString) {
@ -273,52 +273,89 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
} }
} }
/**
// We could have used the built-in PropertiesLoginModule but that exposes a roles properties file. Roles are used * Clients must connect to us with a username and password and must use TLS. If a someone connects with
// for queue access control and our RPC users must only have access to the queues they need and this cannot be allowed * [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
// to be modified. * is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
* the CN within that is their legal name.
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
*/
class NodeLoginModule : LoginModule { class NodeLoginModule : LoginModule {
companion object { companion object {
const val NODE_USER = "Node" // Include forbidden username character to prevent name clash with any RPC usernames
const val PEER_ROLE = "SystemRoles/Peer"
const val NODE_ROLE = "SystemRoles/Node"
const val RPC_ROLE = "SystemRoles/RPC"
} }
private var loginSucceeded: Boolean = false private var loginSucceeded: Boolean = false
private lateinit var subject: Subject private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler private lateinit var callbackHandler: CallbackHandler
private lateinit var userService: RPCUserService private lateinit var userService: RPCUserService
private lateinit var ourRootCAPublicKey: PublicKey
private lateinit var ourPublicKey: PublicKey
private val principals = ArrayList<Principal>() private val principals = ArrayList<Principal>()
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) { override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject this.subject = subject
this.callbackHandler = callbackHandler this.callbackHandler = callbackHandler
userService = options[RPCUserService::class.java.name] as RPCUserService userService = options[RPCUserService::class.java.name] as RPCUserService
ourRootCAPublicKey = options[CORDA_ROOT_CA] as PublicKey
ourPublicKey = options[CORDA_CLIENT_CA] as PublicKey
} }
override fun login(): Boolean { override fun login(): Boolean {
val nameCallback = NameCallback("Username: ") val nameCallback = NameCallback("Username: ")
val passwordCallback = PasswordCallback("Password: ", false) val passwordCallback = PasswordCallback("Password: ", false)
val certificateCallback = CertificateCallback()
try { try {
callbackHandler.handle(arrayOf(nameCallback, passwordCallback)) callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
} catch (e: IOException) { } catch (e: IOException) {
throw LoginException(e.message) throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) { } catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user") throw LoginException("${e.message} not available to obtain information from user")
} }
val username = nameCallback.name ?: throw FailedLoginException("User name is null") val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val receivedPassword = passwordCallback.password ?: throw FailedLoginException("Password is null") val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
val password = if (username == NODE_USER) "Node" else userService.getUser(username)?.password ?: throw FailedLoginException("User does not exist")
if (password != String(receivedPassword)) { val validatedUser = if (username == PEER_USER || username == NODE_USER) {
throw FailedLoginException("Password does not match") val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?")
val peerCertificate = certificates.first()
val role = if (username == NODE_USER) {
if (peerCertificate.publicKey != ourPublicKey) {
throw FailedLoginException("Only the node can login as $NODE_USER")
}
NODE_ROLE
} else {
val theirRootCAPublicKey = certificates.last().publicKey
if (theirRootCAPublicKey != ourRootCAPublicKey) {
throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey")
}
PEER_ROLE // This enables the peer to send to our P2P address
}
principals += RolePrincipal(role)
peerCertificate.subjectDN.name
} else {
// Otherwise assume they're an RPC user
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
if (password != rpcUser.password) {
// TODO Switch to hashed passwords
// TODO Retrieve client IP address to include in exception message
throw FailedLoginException("Password for user $username does not match")
}
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
principals += RolePrincipal("$CLIENTS_PREFIX$username") // This enables the RPC client to receive responses
username
} }
principals += UserPrincipal(username) principals += UserPrincipal(validatedUser)
principals += RolePrincipal(username) // The roles are configured using the usernames
if (username != NODE_USER) {
principals += RolePrincipal(RPC_REQUESTS_QUEUE) // This enables the RPC client to send requests
}
loginSucceeded = true loginSucceeded = true
return loginSucceeded return loginSucceeded
@ -347,5 +384,4 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
loginSucceeded = false loginSucceeded = false
} }
} }
} }

View File

@ -19,6 +19,7 @@ import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.*
import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.InsertStatement
@ -56,15 +57,16 @@ class NodeMessagingClient(override val config: NodeConfiguration,
val database: Database, val database: Database,
val networkMapRegistrationFuture: ListenableFuture<Unit>) : ArtemisMessagingComponent(), MessagingServiceInternal { val networkMapRegistrationFuture: ListenableFuture<Unit>) : ArtemisMessagingComponent(), MessagingServiceInternal {
companion object { companion object {
val log = loggerFor<NodeMessagingClient>() private val log = loggerFor<NodeMessagingClient>()
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic". // This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic".
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint // We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
// that will handle messages, like a URL) with the terminology used by underlying MQ libraries, to avoid // that will handle messages, like a URL) with the terminology used by underlying MQ libraries, to avoid
// confusion. // confusion.
val TOPIC_PROPERTY = "platform-topic" const val TOPIC_PROPERTY = "platform-topic"
const val SESSION_ID_PROPERTY = "session-id"
val SESSION_ID_PROPERTY = "session-id" const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
/** /**
* This should be the only way to generate an ArtemisAddress and that only of the remote NetworkMapService node. * This should be the only way to generate an ArtemisAddress and that only of the remote NetworkMapService node.
@ -77,11 +79,11 @@ class NodeMessagingClient(override val config: NodeConfiguration,
private class InnerState { private class InnerState {
var started = false var started = false
var running = false var running = false
val knownQueues = mutableSetOf<SimpleString>()
var producer: ClientProducer? = null var producer: ClientProducer? = null
var p2pConsumer: ClientConsumer? = null var p2pConsumer: ClientConsumer? = null
var session: ClientSession? = null var session: ClientSession? = null
var clientFactory: ClientSessionFactory? = null var clientFactory: ClientSessionFactory? = null
var rpcDispatcher: RPCDispatcher? = null
// Consumer for inbound client RPC messages. // Consumer for inbound client RPC messages.
var rpcConsumer: ClientConsumer? = null var rpcConsumer: ClientConsumer? = null
var rpcNotificationConsumer: ClientConsumer? = null var rpcNotificationConsumer: ClientConsumer? = null
@ -89,12 +91,12 @@ class NodeMessagingClient(override val config: NodeConfiguration,
/** A registration to handle messages of different types */ /** A registration to handle messages of different types */
data class Handler(val topicSession: TopicSession, data class Handler(val topicSession: TopicSession,
val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
/** /**
* Apart from the NetworkMapService this is the only other address accessible to the node outside of lookups against the NetworkMapCache. * Apart from the NetworkMapService this is the only other address accessible to the node outside of lookups against the NetworkMapCache.
*/ */
override val myAddress: SingleMessageRecipient = toMyAddress(myIdentity, serverHostPort) override val myAddress: SingleMessageRecipient = if (myIdentity != null) NodeAddress(myIdentity, serverHostPort) else NetworkMapAddress(serverHostPort)
private val state = ThreadBox(InnerState()) private val state = ThreadBox(InnerState())
private val handlers = CopyOnWriteArrayList<Handler>() private val handlers = CopyOnWriteArrayList<Handler>()
@ -104,13 +106,12 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
private val processedMessages: MutableSet<UUID> = Collections.synchronizedSet( private val processedMessages: MutableSet<UUID> = Collections.synchronizedSet(
object : AbstractJDBCHashSet<UUID, Table>(Table, loadOnInit = true) { object : AbstractJDBCHashSet<UUID, Table>(Table, loadOnInit = true) {
override fun elementFromRow(row: ResultRow): UUID = row[table.uuid] override fun elementFromRow(row: ResultRow): UUID = row[table.uuid]
override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) {
override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) { insert[table.uuid] = entry
insert[table.uuid] = entry }
} })
})
fun start(rpcOps: RPCOps, userService: RPCUserService) { fun start(rpcOps: RPCOps, userService: RPCUserService) {
state.locked { state.locked {
@ -118,14 +119,15 @@ class NodeMessagingClient(override val config: NodeConfiguration,
started = true started = true
log.info("Connecting to server: $serverHostPort") log.info("Connecting to server: $serverHostPort")
// Connect to our server. TODO: This should use the in-VM transport.
val tcpTransport = tcpTransport(ConnectionDirection.OUTBOUND, serverHostPort.hostText, serverHostPort.port) val tcpTransport = tcpTransport(ConnectionDirection.OUTBOUND, serverHostPort.hostText, serverHostPort.port)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport)
clientFactory = locator.createSessionFactory() clientFactory = locator.createSessionFactory()
// Create a session. Note that the acknowledgement of messages is not flushed to // Login using the node username. The broker will authentiate us as its node (as opposed to another peer)
// the Artermis journal until the default buffer size of 1MB is acknowledged. // using our TLS certificate.
val session = clientFactory!!.createSession("Node", "Node", false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) // Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
// size of 1MB is acknowledged.
val session = clientFactory!!.createSession(NODE_USER, NODE_USER, false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
this.session = session this.session = session
session.start() session.start()
@ -133,22 +135,17 @@ class NodeMessagingClient(override val config: NodeConfiguration,
producer = session.createProducer() producer = session.createProducer()
// Create a queue, consumer and producer for handling P2P network messages. // Create a queue, consumer and producer for handling P2P network messages.
val queueName = toQueueName(myAddress) createQueueIfAbsent(SimpleString(P2P_QUEUE))
val query = session.queueQuery(queueName) p2pConsumer = makeP2PConsumer(session, true)
if (!query.isExists) {
session.createQueue(queueName, queueName, true)
}
knownQueues.add(queueName)
p2pConsumer = makeConsumer(session, queueName, true)
networkMapRegistrationFuture.success { networkMapRegistrationFuture.success {
state.locked { state.locked {
log.info("Network map is complete, so removing filter from Artemis consumer.") log.info("Network map is complete, so removing filter from P2P consumer.")
try { try {
p2pConsumer!!.close() p2pConsumer!!.close()
} catch(e: ActiveMQObjectClosedException) { } catch(e: ActiveMQObjectClosedException) {
// Ignore it: this can happen if the server has gone away before we do. // Ignore it: this can happen if the server has gone away before we do.
} }
p2pConsumer = makeConsumer(session, queueName, false) p2pConsumer = makeP2PConsumer(session, false)
} }
} }
@ -156,10 +153,12 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// bridge) and those clients must have authenticated. We could use a single consumer for everything // bridge) and those clients must have authenticated. We could use a single consumer for everything
// and perhaps we should, but these queues are not worth persisting. // and perhaps we should, but these queues are not worth persisting.
session.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE) session.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
session.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 1") // The custom name for the queue is intentional - we may wish other things to subscribe to the
// NOTIFICATIONS_ADDRESS with different filters in future
session.createTemporaryQueue(NOTIFICATIONS_ADDRESS, RPC_QUEUE_REMOVALS_QUEUE, "_AMQ_NotifType = 1")
rpcConsumer = session.createConsumer(RPC_REQUESTS_QUEUE) rpcConsumer = session.createConsumer(RPC_REQUESTS_QUEUE)
rpcNotificationConsumer = session.createConsumer("rpc.qremovals") rpcNotificationConsumer = session.createConsumer(RPC_QUEUE_REMOVALS_QUEUE)
dispatcher = createRPCDispatcher(rpcOps, userService) rpcDispatcher = createRPCDispatcher(rpcOps, userService)
} }
} }
@ -168,13 +167,13 @@ class NodeMessagingClient(override val config: NodeConfiguration,
* the original and make another without a filter. We do this so that there is a network map in place for all other * the original and make another without a filter. We do this so that there is a network map in place for all other
* message handlers. * message handlers.
*/ */
private fun makeConsumer(session: ClientSession, queueName: SimpleString, networkMapOnly: Boolean): ClientConsumer { private fun makeP2PConsumer(session: ClientSession, networkMapOnly: Boolean): ClientConsumer {
return if (networkMapOnly) { return if (networkMapOnly) {
// Filter for just the network map messages. // Filter for just the network map messages.
val messageFilter = SimpleString("hyphenated_props:$TOPIC_PROPERTY like 'platform.network_map.%'") val messageFilter = "hyphenated_props:$TOPIC_PROPERTY like 'platform.network_map.%'"
session.createConsumer(queueName, messageFilter) session.createConsumer(P2P_QUEUE, messageFilter)
} else } else
session.createConsumer(queueName) session.createConsumer(P2P_QUEUE)
} }
private var shutdownLatch = CountDownLatch(1) private var shutdownLatch = CountDownLatch(1)
@ -194,7 +193,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
null null
} ?: return false } ?: return false
val message: Message? = artemisToCordaMessage(artemisMessage) val message: ReceivedMessage? = artemisToCordaMessage(artemisMessage)
if (message != null) if (message != null)
deliver(message) deliver(message)
@ -217,11 +216,10 @@ class NodeMessagingClient(override val config: NodeConfiguration,
private fun runPreNetworkMap() { private fun runPreNetworkMap() {
val consumer = state.locked { val consumer = state.locked {
check(started) check(started) { "start must be called first" }
check(!running) { "run can't be called twice" } check(!running) { "run can't be called twice" }
running = true running = true
// Optionally, start RPC dispatch. rpcDispatcher!!.start(rpcConsumer!!, rpcNotificationConsumer!!, executor)
dispatcher?.start(rpcConsumer!!, rpcNotificationConsumer!!, executor)
p2pConsumer!! p2pConsumer!!
} }
@ -254,7 +252,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
shutdownLatch.countDown() shutdownLatch.countDown()
} }
private fun artemisToCordaMessage(message: ClientMessage): Message? { private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? {
try { try {
if (!message.containsProperty(TOPIC_PROPERTY)) { if (!message.containsProperty(TOPIC_PROPERTY)) {
log.warn("Received message without a $TOPIC_PROPERTY property, ignoring") log.warn("Received message without a $TOPIC_PROPERTY property, ignoring")
@ -268,17 +266,18 @@ class NodeMessagingClient(override val config: NodeConfiguration,
val sessionID = message.getLongProperty(SESSION_ID_PROPERTY) val sessionID = message.getLongProperty(SESSION_ID_PROPERTY)
// Use the magic deduplication property built into Artemis as our message identity too // Use the magic deduplication property built into Artemis as our message identity too
val uuid = UUID.fromString(message.getStringProperty(HDR_DUPLICATE_DETECTION_ID)) val uuid = UUID.fromString(message.getStringProperty(HDR_DUPLICATE_DETECTION_ID))
val user = message.getStringProperty(HDR_VALIDATED_USER) val user = requireNotNull(message.getStringProperty(HDR_VALIDATED_USER)) { "Message is not authenticated" }
log.info("Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid") log.info("Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid")
val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) } val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }
val msg = object : Message { val msg = object : ReceivedMessage {
override val topicSession = TopicSession(topic, sessionID) override val topicSession = TopicSession(topic, sessionID)
override val data: ByteArray = body override val data: ByteArray = body
override val peer: X500Name = X500Name(user)
override val debugTimestamp: Instant = Instant.ofEpochMilli(message.timestamp) override val debugTimestamp: Instant = Instant.ofEpochMilli(message.timestamp)
override val uniqueMessageId: UUID = uuid override val uniqueMessageId: UUID = uuid
override fun toString() = topic + "#" + data.opaque() override fun toString() = "$topic#${data.opaque()}"
} }
return msg return msg
@ -288,7 +287,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
} }
private fun deliver(msg: Message): Boolean { private fun deliver(msg: ReceivedMessage): Boolean {
state.checkNotLocked() state.checkNotLocked()
// Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added // Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added
// or removed whilst the filter is executing will not affect anything. // or removed whilst the filter is executing will not affect anything.
@ -324,7 +323,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
return true return true
} }
private fun callHandlers(msg: Message, deliverTo: List<Handler>) { private fun callHandlers(msg: ReceivedMessage, deliverTo: List<Handler>) {
for (handler in deliverTo) { for (handler in deliverTo) {
handler.callback(msg, handler) handler.callback(msg, handler)
} }
@ -368,40 +367,54 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
override fun send(message: Message, target: MessageRecipients) { override fun send(message: Message, target: MessageRecipients) {
val queueName = toQueueName(target)
state.locked { state.locked {
val mqAddress = getMQAddress(target)
val artemisMessage = session!!.createMessage(true).apply { val artemisMessage = session!!.createMessage(true).apply {
val sessionID = message.topicSession.sessionID val sessionID = message.topicSession.sessionID
putStringProperty(TOPIC_PROPERTY, message.topicSession.topic) putStringProperty(TOPIC_PROPERTY, message.topicSession.topic)
putLongProperty(SESSION_ID_PROPERTY, sessionID) putLongProperty(SESSION_ID_PROPERTY, sessionID)
writeBodyBufferBytes(message.data) writeBodyBufferBytes(message.data)
// Use the magic deduplication property built into Artemis as our message identity too // Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(message.uniqueMessageId.toString()))
} }
if (knownQueues.add(queueName)) { log.info("Send to: $mqAddress topic: ${message.topicSession.topic} sessionID: ${message.topicSession.sessionID} uuid: ${message.uniqueMessageId}")
maybeCreateQueue(queueName) producer!!.send(mqAddress, artemisMessage)
}
log.info("send to: $queueName topic: ${message.topicSession.topic} sessionID: ${message.topicSession.sessionID} uuid: ${message.uniqueMessageId}")
producer!!.send(queueName, artemisMessage)
} }
} }
private fun maybeCreateQueue(queueName: SimpleString) { private fun getMQAddress(target: MessageRecipients): SimpleString {
return if (target == myAddress) {
// If we are sending to ourselves then route the message directly to our P2P queue.
SimpleString(P2P_QUEUE)
} else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue.
val internalTargetQueue = (target as? ArtemisAddress)?.queueName ?: throw IllegalArgumentException("Not an Artemis address")
createQueueIfAbsent(internalTargetQueue)
internalTargetQueue
}
}
/** Attempts to create a durable queue on the broker which is bound to an address of the same name. */
private fun createQueueIfAbsent(queueName: SimpleString) {
state.alreadyLocked { state.alreadyLocked {
val queueQuery = session!!.queueQuery(queueName) val queueQuery = session!!.queueQuery(queueName)
if (!queueQuery.isExists) { if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName") log.info("Create fresh queue $queueName bound on same address")
session!!.createQueue(queueName, queueName, true /* durable */) session!!.createQueue(queueName, queueName, true)
} }
} }
} }
override fun addMessageHandler(topic: String, sessionID: Long, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration override fun addMessageHandler(topic: String,
= addMessageHandler(TopicSession(topic, sessionID), callback) sessionID: Long,
callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
return addMessageHandler(TopicSession(topic, sessionID), callback)
}
override fun addMessageHandler(topicSession: TopicSession, override fun addMessageHandler(topicSession: TopicSession,
callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration { callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
require(!topicSession.isBlank()) { "Topic must not be blank, as the empty topic is a special case." } require(!topicSession.isBlank()) { "Topic must not be blank, as the empty topic is a special case." }
val handler = Handler(topicSession, callback) val handler = Handler(topicSession, callback)
handlers.add(handler) handlers.add(handler)
@ -423,8 +436,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
} }
var dispatcher: RPCDispatcher? = null
private fun createRPCDispatcher(ops: RPCOps, userService: RPCUserService) = object : RPCDispatcher(ops, userService) { private fun createRPCDispatcher(ops: RPCOps, userService: RPCUserService) = object : RPCDispatcher(ops, userService) {
override fun send(data: SerializedBytes<*>, toAddress: String) { override fun send(data: SerializedBytes<*>, toAddress: String) {
state.locked { state.locked {

View File

@ -143,8 +143,8 @@ abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService) {
/** Convert an Artemis [ClientMessage] to a MQ-neutral [ClientRPCRequestMessage]. */ /** Convert an Artemis [ClientMessage] to a MQ-neutral [ClientRPCRequestMessage]. */
private fun ClientMessage.toRPCRequestMessage(): ClientRPCRequestMessage { private fun ClientMessage.toRPCRequestMessage(): ClientRPCRequestMessage {
val user = getUser(this) val user = getUser(this)
val replyTo = getAuthenticatedAddress(user, ClientRPCRequestMessage.REPLY_TO, true)!! val replyTo = getReturnAddress(user, ClientRPCRequestMessage.REPLY_TO, true)!!
val observationsTo = getAuthenticatedAddress(user, ClientRPCRequestMessage.OBSERVATIONS_TO, false) val observationsTo = getReturnAddress(user, ClientRPCRequestMessage.OBSERVATIONS_TO, false)
val argBytes = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) } val argBytes = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }
if (argBytes.isEmpty()) { if (argBytes.isEmpty()) {
throw RPCException("empty serialized args") throw RPCException("empty serialized args")
@ -158,12 +158,11 @@ abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService) {
return userService.getUser(message.requiredString(Message.HDR_VALIDATED_USER.toString()))!! return userService.getUser(message.requiredString(Message.HDR_VALIDATED_USER.toString()))!!
} }
private fun ClientMessage.getAuthenticatedAddress(user: User, property: String, required: Boolean): String? { private fun ClientMessage.getReturnAddress(user: User, property: String, required: Boolean): String? {
val address: String? = if (required) requiredString(property) else getStringProperty(property) return if (containsProperty(property)) {
val expectedAddressPrefix = "${ArtemisMessagingComponent.CLIENTS_PREFIX}${user.username}." "${ArtemisMessagingComponent.CLIENTS_PREFIX}${user.username}.rpc.${getLongProperty(property)}"
if (address != null && !address.startsWith(expectedAddressPrefix)) { } else {
throw RPCException("$property address does not match up with the user") if (required) throw RPCException("missing $property property") else null
} }
return address
} }
} }

View File

@ -196,7 +196,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
val session = FlowSession(sessionFlow, nodeIdentity, random63BitValue(), null) val session = FlowSession(sessionFlow, nodeIdentity, random63BitValue(), null)
openSessions[Pair(sessionFlow, nodeIdentity)] = session openSessions[Pair(sessionFlow, nodeIdentity)] = session
val counterpartyFlow = sessionFlow.getCounterpartyMarker(nodeIdentity).name val counterpartyFlow = sessionFlow.getCounterpartyMarker(nodeIdentity).name
val sessionInit = SessionInit(session.ourSessionId, serviceHub.myInfo.legalIdentity, counterpartyFlow, firstPayload) val sessionInit = SessionInit(session.ourSessionId, counterpartyFlow, firstPayload)
val sessionInitResponse = sendAndReceiveInternal<SessionInitResponse>(session, sessionInit) val sessionInitResponse = sendAndReceiveInternal<SessionInitResponse>(session, sessionInit)
if (sessionInitResponse is SessionConfirm) { if (sessionInitResponse is SessionConfirm) {
session.otherPartySessionId = sessionInitResponse.initiatedSessionId session.otherPartySessionId = sessionInitResponse.initiatedSessionId

View File

@ -211,7 +211,18 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
val sessionMessage = message.data.deserialize<SessionMessage>() val sessionMessage = message.data.deserialize<SessionMessage>()
when (sessionMessage) { when (sessionMessage) {
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage) is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage)
is SessionInit -> onSessionInit(sessionMessage) is SessionInit -> {
// TODO SECURITY Look up the party with the full X.500 name instead of just the legal name which
// isn't required to be unique
// TODO For now have the doorman block signups with identical names, and names with characters that
// are used in X.500 name textual serialisation
val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peerLegalName)?.legalIdentity
if (otherParty != null) {
onSessionInit(sessionMessage, otherParty)
} else {
logger.error("Unknown peer ${message.peer} in $sessionMessage")
}
}
} }
} }
} }
@ -254,10 +265,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
} }
} }
private fun onSessionInit(sessionInit: SessionInit) { private fun onSessionInit(sessionInit: SessionInit, otherParty: Party) {
logger.trace { "Received $sessionInit" } logger.trace { "Received $sessionInit $otherParty" }
//TODO Verify the other party are who they say they are from the TLS subsystem
val otherParty = sessionInit.initiatorParty
val otherPartySessionId = sessionInit.initiatorSessionId val otherPartySessionId = sessionInit.initiatorSessionId
try { try {
val markerClass = Class.forName(sessionInit.flowName) val markerClass = Class.forName(sessionInit.flowName)
@ -446,10 +455,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
val recipientSessionId: Long val recipientSessionId: Long
} }
data class SessionInit(val initiatorSessionId: Long, data class SessionInit(val initiatorSessionId: Long, val flowName: String, val firstPayload: Any?) : SessionMessage
val initiatorParty: Party,
val flowName: String,
val firstPayload: Any?) : SessionMessage
interface SessionInitResponse : ExistingSessionMessage interface SessionInitResponse : ExistingSessionMessage

View File

@ -1,5 +1,3 @@
@file:Suppress("UNUSED_VARIABLE")
package net.corda.node.messaging package net.corda.node.messaging
import net.corda.core.messaging.Message import net.corda.core.messaging.Message
@ -9,7 +7,6 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.junit.Before
import org.junit.Test import org.junit.Test
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -17,12 +14,7 @@ import kotlin.test.assertFails
import kotlin.test.assertTrue import kotlin.test.assertTrue
class InMemoryMessagingTests { class InMemoryMessagingTests {
lateinit var network: MockNetwork val network = MockNetwork()
@Before
fun setUp() {
network = MockNetwork()
}
@Test @Test
fun topicStringValidation() { fun topicStringValidation() {
@ -115,6 +107,5 @@ class InMemoryMessagingTests {
node2.net.send(validMessage2, node1.net.myAddress) node2.net.send(validMessage2, node1.net.myAddress)
network.runNetwork() network.runNetwork()
assertEquals(2, received) assertEquals(2, received)
} }
} }

View File

@ -13,17 +13,19 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID
import net.corda.core.utilities.LogHelper import net.corda.core.utilities.LogHelper
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.messaging.RPCOps import net.corda.node.services.messaging.RPCOps
import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.InMemoryNetworkMapCache
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.junit.After import org.junit.After
@ -39,7 +41,6 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue
class ArtemisMessagingTests { class ArtemisMessagingTests {
@Rule @JvmField val temporaryFolder = TemporaryFolder() @Rule @JvmField val temporaryFolder = TemporaryFolder()
@ -57,7 +58,6 @@ class ArtemisMessagingTests {
var messagingClient: NodeMessagingClient? = null var messagingClient: NodeMessagingClient? = null
var messagingServer: ArtemisMessagingServer? = null var messagingServer: ArtemisMessagingServer? = null
val networkMapCache = InMemoryNetworkMapCache() val networkMapCache = InMemoryNetworkMapCache()
val rpcOps = object : RPCOps { val rpcOps = object : RPCOps {
@ -196,7 +196,7 @@ class ArtemisMessagingTests {
createAndStartClientAndServer(receivedMessages) createAndStartClientAndServer(receivedMessages)
for (iter in 1..iterations) { for (iter in 1..iterations) {
val firstActual: Message = receivedMessages.take() val firstActual: Message = receivedMessages.take()
assertTrue(String(firstActual.data).equals("first msg $iter")) assertThat(String(firstActual.data)).isEqualTo("first msg $iter")
} }
assertNull(receivedMessages.poll(200, MILLISECONDS)) assertNull(receivedMessages.poll(200, MILLISECONDS))
} }
@ -223,16 +223,22 @@ class ArtemisMessagingTests {
private fun createMessagingClient(server: HostAndPort = hostAndPort): NodeMessagingClient { private fun createMessagingClient(server: HostAndPort = hostAndPort): NodeMessagingClient {
return databaseTransaction(database) { return databaseTransaction(database) {
NodeMessagingClient(config, server, identity.public.composite, AffinityExecutor.ServiceAffinityExecutor("ArtemisMessagingTests", 1), database, networkMapRegistrationFuture).apply { NodeMessagingClient(
configureWithDevSSLCertificate() config,
server,
identity.public.composite,
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
database,
networkMapRegistrationFuture).apply {
config.configureWithDevSSLCertificate()
messagingClient = this messagingClient = this
} }
} }
} }
private fun createMessagingServer(local: HostAndPort = hostAndPort): ArtemisMessagingServer { private fun createMessagingServer(local: HostAndPort = hostAndPort): ArtemisMessagingServer {
return ArtemisMessagingServer(config, local, identity.public.composite, networkMapCache, userService).apply { return ArtemisMessagingServer(config, local, networkMapCache, userService).apply {
configureWithDevSSLCertificate() config.configureWithDevSSLCertificate()
messagingServer = this messagingServer = this
} }
} }

View File

@ -197,14 +197,14 @@ class StateMachineManagerTests {
assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload) assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload)
assertSessionTransfers(node2, assertSessionTransfers(node2,
node1 sent sessionInit(node1, SendFlow::class, payload) to node2, node1 sent sessionInit(SendFlow::class, payload) to node2,
node2 sent sessionConfirm() to node1, node2 sent sessionConfirm() to node1,
node1 sent sessionEnd() to node2 node1 sent sessionEnd() to node2
//There's no session end from the other flows as they're manually suspended //There's no session end from the other flows as they're manually suspended
) )
assertSessionTransfers(node3, assertSessionTransfers(node3,
node1 sent sessionInit(node1, SendFlow::class, payload) to node3, node1 sent sessionInit(SendFlow::class, payload) to node3,
node3 sent sessionConfirm() to node1, node3 sent sessionConfirm() to node1,
node1 sent sessionEnd() to node3 node1 sent sessionEnd() to node3
//There's no session end from the other flows as they're manually suspended //There's no session end from the other flows as they're manually suspended
@ -230,14 +230,14 @@ class StateMachineManagerTests {
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload) assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload)
assertSessionTransfers(node2, assertSessionTransfers(node2,
node1 sent sessionInit(node1, ReceiveThenSuspendFlow::class) to node2, node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node2,
node2 sent sessionConfirm() to node1, node2 sent sessionConfirm() to node1,
node2 sent sessionData(node2Payload) to node1, node2 sent sessionData(node2Payload) to node1,
node2 sent sessionEnd() to node1 node2 sent sessionEnd() to node1
) )
assertSessionTransfers(node3, assertSessionTransfers(node3,
node1 sent sessionInit(node1, ReceiveThenSuspendFlow::class) to node3, node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node3,
node3 sent sessionConfirm() to node1, node3 sent sessionConfirm() to node1,
node3 sent sessionData(node3Payload) to node1, node3 sent sessionData(node3Payload) to node1,
node3 sent sessionEnd() to node1 node3 sent sessionEnd() to node1
@ -251,7 +251,7 @@ class StateMachineManagerTests {
net.runNetwork() net.runNetwork()
assertSessionTransfers( assertSessionTransfers(
node1 sent sessionInit(node1, PingPongFlow::class, 10L) to node2, node1 sent sessionInit(PingPongFlow::class, 10L) to node2,
node2 sent sessionConfirm() to node1, node2 sent sessionConfirm() to node1,
node2 sent sessionData(20L) to node1, node2 sent sessionData(20L) to node1,
node1 sent sessionData(11L) to node2, node1 sent sessionData(11L) to node2,
@ -267,7 +267,7 @@ class StateMachineManagerTests {
net.runNetwork() net.runNetwork()
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(FlowSessionException::class.java) assertThatThrownBy { future.getOrThrow() }.isInstanceOf(FlowSessionException::class.java)
assertSessionTransfers( assertSessionTransfers(
node1 sent sessionInit(node1, ReceiveThenSuspendFlow::class) to node2, node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node2,
node2 sent sessionConfirm() to node1, node2 sent sessionConfirm() to node1,
node2 sent sessionEnd() to node1 node2 sent sessionEnd() to node1
) )
@ -288,9 +288,7 @@ class StateMachineManagerTests {
return smm.findStateMachines(P::class.java).single() return smm.findStateMachines(P::class.java).single()
} }
private fun sessionInit(initiatorNode: MockNode, flowMarker: KClass<*>, payload: Any? = null): SessionInit { private fun sessionInit(flowMarker: KClass<*>, payload: Any? = null) = SessionInit(0, flowMarker.java.name, payload)
return SessionInit(0, initiatorNode.info.legalIdentity, flowMarker.java.name, payload)
}
private fun sessionConfirm() = SessionConfirm(0, 0) private fun sessionConfirm() = SessionConfirm(0, 0)
@ -314,7 +312,7 @@ class StateMachineManagerTests {
private fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> { private fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> {
return filter { it.message.topicSession == StateMachineManager.sessionTopic }.map { return filter { it.message.topicSession == StateMachineManager.sessionTopic }.map {
val from = it.sender.myAddress.id val from = it.sender.id
val message = it.message.data.deserialize<SessionMessage>() val message = it.message.data.deserialize<SessionMessage>()
val to = (it.recipients as InMemoryMessagingNetwork.Handle).id val to = (it.recipients as InMemoryMessagingNetwork.Handle).id
SessionTransfer(from, sanitise(message), to) SessionTransfer(from, sanitise(message), to)
@ -371,7 +369,6 @@ class StateMachineManagerTests {
@Suspendable @Suspendable
override fun call() { override fun call() {
receivedPayloads = otherParties.map { receive<Any>(it).unwrap { it } } receivedPayloads = otherParties.map { receive<Any>(it).unwrap { it } }
println(receivedPayloads)
Fiber.park() Fiber.park()
} }
} }
@ -384,9 +381,7 @@ class StateMachineManagerTests {
@Suspendable @Suspendable
override fun call() { override fun call() {
receivedPayload = sendAndReceive<Long>(otherParty, payload).unwrap { it } receivedPayload = sendAndReceive<Long>(otherParty, payload).unwrap { it }
println("${fsm.id} Received $receivedPayload")
receivedPayload2 = sendAndReceive<Long>(otherParty, payload + 1).unwrap { it } receivedPayload2 = sendAndReceive<Long>(otherParty, payload + 1).unwrap { it }
println("${fsm.id} Received $receivedPayload2")
} }
} }

View File

@ -109,7 +109,7 @@ class NetworkMapVisualiser : Application() {
} }
// Fire the message bullets between nodes. // Fire the message bullets between nodes.
simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer -> simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender.myAddress) val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender)
val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient) val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
if (transferIsInteresting(msg)) { if (transferIsInteresting(msg)) {
@ -345,7 +345,7 @@ class NetworkMapVisualiser : Application() {
private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean { private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean {
// Loopback messages are boring. // Loopback messages are boring.
if (transfer.sender.myAddress == transfer.recipients) return false if (transfer.sender == transfer.recipients) return false
// Network map push acknowledgements are boring. // Network map push acknowledgements are boring.
if (NetworkMapService.PUSH_ACK_FLOW_TOPIC in transfer.message.topicSession.topic) return false if (NetworkMapService.PUSH_ACK_FLOW_TOPIC in transfer.message.topicSession.topic) return false
val message = transfer.message.data.deserialize<Any>() val message = transfer.message.data.deserialize<Any>()

View File

@ -0,0 +1,36 @@
package net.corda.testing.messaging
import com.google.common.net.HostAndPort
import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.OUTBOUND
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: HostAndPort) : ArtemisMessagingComponent() {
override val config: NodeSSLConfiguration = configureTestSSL()
lateinit var sessionFactory: ClientSessionFactory
lateinit var session: ClientSession
lateinit var producer: ClientProducer
fun start(username: String? = null, password: String? = null) {
val tcpTransport = tcpTransport(OUTBOUND, target.hostText, target.port)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
isBlockOnNonDurableSend = true
threadPoolMaxSize = 1
}
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() {
sessionFactory.close()
}
}

View File

@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.crypto.X509Utilities
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
@ -14,6 +15,7 @@ import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.JDBCHashSet import net.corda.node.utilities.JDBCHashSet
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
import net.corda.testing.node.InMemoryMessagingNetwork.InMemoryMessaging import net.corda.testing.node.InMemoryMessagingNetwork.InMemoryMessaging
import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Observable import rx.Observable
@ -43,8 +45,8 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
private var counter = 0 // -1 means stopped. private var counter = 0 // -1 means stopped.
private val handleEndpointMap = HashMap<Handle, InMemoryMessaging>() private val handleEndpointMap = HashMap<Handle, InMemoryMessaging>()
data class MessageTransfer(val sender: InMemoryMessaging, val message: Message, val recipients: MessageRecipients) { data class MessageTransfer(val sender: Handle, val message: Message, val recipients: MessageRecipients) {
override fun toString() = "${message.topicSession} from '${sender.myAddress}' to '$recipients'" override fun toString() = "${message.topicSession} from '$sender' to '$recipients'"
} }
// All sent messages are kept here until pumpSend is called, or manuallyPumped is set to false // All sent messages are kept here until pumpSend is called, or manuallyPumped is set to false
@ -85,8 +87,7 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
@Synchronized @Synchronized
fun createNode(manuallyPumped: Boolean, fun createNode(manuallyPumped: Boolean,
executor: AffinityExecutor, executor: AffinityExecutor,
database: Database) database: Database): Pair<Handle, MessagingServiceBuilder<InMemoryMessaging>> {
: Pair<Handle, MessagingServiceBuilder<InMemoryMessaging>> {
check(counter >= 0) { "In memory network stopped: please recreate." } check(counter >= 0) { "In memory network stopped: please recreate." }
val builder = createNodeWithID(manuallyPumped, counter, executor, database = database) as Builder val builder = createNodeWithID(manuallyPumped, counter, executor, database = database) as Builder
counter++ counter++
@ -118,8 +119,7 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
@Synchronized @Synchronized
private fun msgSend(from: InMemoryMessaging, message: Message, recipients: MessageRecipients) { private fun msgSend(from: InMemoryMessaging, message: Message, recipients: MessageRecipients) {
val transfer = MessageTransfer(from, message, recipients) messageSendQueue += MessageTransfer(from.myAddress, message, recipients)
messageSendQueue.add(transfer)
} }
@Synchronized @Synchronized
@ -164,15 +164,13 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
// If block is set to true this function will only return once a message has been pushed onto the recipients' queues // If block is set to true this function will only return once a message has been pushed onto the recipients' queues
fun pumpSend(block: Boolean): MessageTransfer? { fun pumpSend(block: Boolean): MessageTransfer? {
val transfer = (if (block) messageSendQueue.take() else messageSendQueue.poll()) ?: return null val transfer = (if (block) messageSendQueue.take() else messageSendQueue.poll()) ?: return null
val recipients = transfer.recipients
val from = transfer.sender.myAddress
log.trace { transfer.toString() } log.trace { transfer.toString() }
val calc = latencyCalculator val calc = latencyCalculator
if (calc != null && recipients is SingleMessageRecipient) { if (calc != null && transfer.recipients is SingleMessageRecipient) {
val messageSent = SettableFuture.create<Unit>() val messageSent = SettableFuture.create<Unit>()
// Inject some artificial latency. // Inject some artificial latency.
timer.schedule(calc.between(from, recipients).toMillis()) { timer.schedule(calc.between(transfer.sender, transfer.recipients).toMillis()) {
pumpSendInternal(transfer) pumpSendInternal(transfer)
messageSent.set(Unit) messageSent.set(Unit)
} }
@ -189,7 +187,6 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
fun pumpSendInternal(transfer: MessageTransfer) { fun pumpSendInternal(transfer: MessageTransfer) {
when (transfer.recipients) { when (transfer.recipients) {
is Handle -> getQueueForHandle(transfer.recipients).add(transfer) is Handle -> getQueueForHandle(transfer.recipients).add(transfer)
is AllPossibleRecipients -> { is AllPossibleRecipients -> {
// This means all possible recipients _that the network knows about at the time_, not literally everyone // This means all possible recipients _that the network knows about at the time_, not literally everyone
// who joins into the indefinite future. // who joins into the indefinite future.
@ -214,14 +211,14 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
private val executor: AffinityExecutor, private val executor: AffinityExecutor,
private val database: Database) : SingletonSerializeAsToken(), MessagingServiceInternal { private val database: Database) : SingletonSerializeAsToken(), MessagingServiceInternal {
inner class Handler(val topicSession: TopicSession, inner class Handler(val topicSession: TopicSession,
val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
@Volatile @Volatile
private var running = true private var running = true
private inner class InnerState { private inner class InnerState {
val handlers: MutableList<Handler> = ArrayList() val handlers: MutableList<Handler> = ArrayList()
val pendingRedelivery = JDBCHashSet<Message>("pending_messages",loadOnInit = true) val pendingRedelivery = JDBCHashSet<MessageTransfer>("pending_messages", loadOnInit = true)
} }
private val state = ThreadBox(InnerState()) private val state = ThreadBox(InnerState())
@ -240,23 +237,22 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
} }
} }
override fun addMessageHandler(topic: String, sessionID: Long, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration override fun addMessageHandler(topic: String, sessionID: Long, callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
= addMessageHandler(TopicSession(topic, sessionID), callback) = addMessageHandler(TopicSession(topic, sessionID), callback)
override fun addMessageHandler(topicSession: TopicSession, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration { override fun addMessageHandler(topicSession: TopicSession, callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
check(running) check(running)
val (handler, items) = state.locked { val (handler, transfers) = state.locked {
val handler = Handler(topicSession, callback).apply { handlers.add(this) } val handler = Handler(topicSession, callback).apply { handlers.add(this) }
val pending = ArrayList<Message>() val pending = ArrayList<MessageTransfer>()
databaseTransaction(database) { databaseTransaction(database) {
pending.addAll(pendingRedelivery) pending.addAll(pendingRedelivery)
pendingRedelivery.clear() pendingRedelivery.clear()
} }
Pair(handler, pending) Pair(handler, pending)
} }
for (message in items) {
send(message, handle) transfers.forEach { pumpSendInternal(it) }
}
return handler return handler
} }
@ -323,9 +319,8 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
while (deliverTo == null) { while (deliverTo == null) {
val transfer = (if (block) q.take() else q.poll()) ?: return null val transfer = (if (block) q.take() else q.poll()) ?: return null
deliverTo = state.locked { deliverTo = state.locked {
val h = handlers.filter { if (it.topicSession.isBlank()) true else transfer.message.topicSession == it.topicSession } val matchingHandlers = handlers.filter { it.topicSession.isBlank() || transfer.message.topicSession == it.topicSession }
if (matchingHandlers.isEmpty()) {
if (h.isEmpty()) {
// Got no handlers for this message yet. Keep the message around and attempt redelivery after a new // Got no handlers for this message yet. Keep the message around and attempt redelivery after a new
// handler has been registered. The purpose of this path is to make unit tests that have multi-threading // handler has been registered. The purpose of this path is to make unit tests that have multi-threading
// reliable, as a sender may attempt to send a message to a receiver that hasn't finished setting // reliable, as a sender may attempt to send a message to a receiver that hasn't finished setting
@ -333,11 +328,11 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
// least sometimes. // least sometimes.
log.warn("Message to ${transfer.message.topicSession} could not be delivered") log.warn("Message to ${transfer.message.topicSession} could not be delivered")
databaseTransaction(database) { databaseTransaction(database) {
pendingRedelivery.add(transfer.message) pendingRedelivery.add(transfer)
} }
null null
} else { } else {
h matchingHandlers
} }
} }
if (deliverTo != null) { if (deliverTo != null) {
@ -357,7 +352,7 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
databaseTransaction(database) { databaseTransaction(database) {
for (handler in deliverTo) { for (handler in deliverTo) {
try { try {
handler.callback(transfer.message, handler) handler.callback(transfer.toReceivedMessage(), handler)
} catch(e: Exception) { } catch(e: Exception) {
log.error("Caught exception in handler for $this/${handler.topicSession}", e) log.error("Caught exception in handler for $this/${handler.topicSession}", e)
} }
@ -371,5 +366,13 @@ class InMemoryMessagingNetwork(val sendManuallyPumped: Boolean) : SingletonSeria
} }
return transfer return transfer
} }
private fun MessageTransfer.toReceivedMessage() = object : ReceivedMessage {
override val topicSession: TopicSession get() = message.topicSession
override val data: ByteArray get() = message.data
override val peer: X500Name get() = X509Utilities.getDevX509Name(sender.description)
override val debugTimestamp: Instant get() = message.debugTimestamp
override val uniqueMessageId: UUID get() = message.uniqueMessageId
}
} }
} }

View File

@ -0,0 +1,69 @@
package net.corda.testing.node
import net.corda.core.getOrThrow
import net.corda.node.internal.Node
import net.corda.node.services.User
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.testing.freeLocalHostAndPort
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.util.*
import kotlin.concurrent.thread
/**
* Extend this class if you need to run nodes in a test. You could use the driver DSL but it's extremely slow for testing
* purposes.
*/
abstract class NodeBasedTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private val nodes = ArrayList<Node>()
lateinit var networkMapNode: Node
@Before
fun startNetworkMapNode() {
networkMapNode = startNode("Network Map", emptyMap())
}
@After
fun stopNodes() {
nodes.forEach(Node::stop)
}
fun startNode(legalName: String, rpcUsers: List<User> = emptyList()): Node {
return startNode(legalName, mapOf(
"networkMapAddress" to networkMapNode.configuration.artemisAddress.toString(),
"rpcUsers" to rpcUsers.map { mapOf(
"user" to it.username,
"password" to it.password,
"permissions" to it.permissions)
}
))
}
private fun startNode(legalName: String, config: Map<String, Any>): Node {
val config = ConfigHelper.loadConfig(
baseDirectoryPath = tempFolder.newFolder(legalName).toPath(),
allowMissingConfig = true,
configOverrides = config + mapOf(
"myLegalName" to legalName,
"artemisAddress" to freeLocalHostAndPort().toString(),
"extraAdvertisedServiceIds" to ""
)
)
val node = FullNodeConfiguration(config).createNode()
node.start()
nodes += node
thread(name = legalName) {
node.run()
}
node.networkMapRegistrationFuture.getOrThrow()
return node
}
}