diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 5d2effcb9a..2f4dc7fa74 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -15,6 +15,14 @@
+
+
+
+
+
+
+
+
diff --git a/bridge/bridgecapsule/build.gradle b/bridge/bridgecapsule/build.gradle
new file mode 100644
index 0000000000..f5c6c98669
--- /dev/null
+++ b/bridge/bridgecapsule/build.gradle
@@ -0,0 +1,65 @@
+/**
+ * This build.gradle exists to publish our capsule (executable fat jar) to maven. It cannot be placed in the
+ * bridges project because the bintray plugin cannot publish two modules from one project.
+ */
+apply plugin: 'net.corda.plugins.publish-utils'
+apply plugin: 'us.kirchmeier.capsule'
+
+description 'Corda bridge server capsule'
+
+configurations {
+ runtimeArtifacts
+ capsuleRuntime
+}
+
+dependencies {
+ // TypeSafe Config: for simple and human friendly config files.
+ capsuleRuntime "com.typesafe:config:$typesafe_config_version"
+}
+
+// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards
+// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
+// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
+// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into
+// its own module so its target can be controlled individually, but for now this suffices.
+sourceCompatibility = 1.6
+targetCompatibility = 1.6
+
+task buildBridgeServerJar(type: FatCapsule, dependsOn: project(':bridge').jar) {
+ applicationClass 'net.corda.bridge.Bridge'
+ archiveName "corda-bridgeserver-${corda_release_version}.jar"
+ applicationSource = files(
+ project(':bridge').configurations.runtime,
+ project(':bridge').jar,
+ "$rootDir/config/dev/log4j2.xml",
+ "$rootDir/bridge/build/resources/main/reference.conf"
+ )
+ from 'NOTICE' // Copy CDDL notice
+ from configurations.capsuleRuntime.files.collect { zipTree(it) }
+
+ capsuleManifest {
+ applicationVersion = corda_release_version
+ javaAgents = []
+ systemProperties['visualvm.display.name'] = 'Corda Bridge Server'
+ minJavaVersion = '1.8.0'
+ minUpdateVersion['1.8'] = java8_minUpdateVersion
+ caplets = []
+
+ // JVM configuration:
+ // - Constrain to small heap sizes to ease development on low end devices.
+ // - Switch to the G1 GC which is going to be the default in Java 9 and gives low pause times/string dedup.
+ jvmArgs = ['-Xmx200m', '-XX:+UseG1GC']
+ }
+}
+
+artifacts {
+ runtimeArtifacts buildBridgeServerJar
+ publish buildBridgeServerJar {
+ classifier ""
+ }
+}
+
+publish {
+ disableDefaultJar = true
+ name 'corda-bridgeserver'
+}
diff --git a/bridge/build.gradle b/bridge/build.gradle
new file mode 100644
index 0000000000..0d0e8616cb
--- /dev/null
+++ b/bridge/build.gradle
@@ -0,0 +1,56 @@
+apply plugin: 'kotlin'
+apply plugin: 'java'
+apply plugin: 'net.corda.plugins.publish-utils'
+
+description 'Corda peer bridging components'
+
+configurations {
+ integrationTestCompile.extendsFrom testCompile
+ integrationTestRuntime.extendsFrom testRuntime
+}
+
+sourceSets {
+ integrationTest {
+ kotlin {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ srcDir file('src/integration-test/kotlin')
+ }
+ }
+}
+
+processResources {
+ from file("$rootDir/config/dev/log4j2.xml")
+}
+
+dependencies {
+ compile project(':node-api')
+
+ // Log4J: logging framework (with SLF4J bindings)
+ compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
+ compile "org.apache.logging.log4j:log4j-core:$log4j_version"
+ compile "org.slf4j:jul-to-slf4j:$slf4j_version"
+
+ // JOpt: for command line flags.
+ compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
+
+ // Manifests: for reading stuff from the manifest file
+ compile "com.jcabi:jcabi-manifests:1.1"
+
+ integrationTestCompile project(':node-driver')
+ testCompile "junit:junit:$junit_version"
+ testCompile project(':test-utils')
+}
+
+task integrationTest(type: Test) {
+ testClassesDirs = sourceSets.integrationTest.output.classesDirs
+ classpath = sourceSets.integrationTest.runtimeClasspath
+}
+
+jar {
+ baseName 'corda-bridge-impl'
+}
+
+publish {
+ name jar.baseName
+}
diff --git a/bridge/src/integration-test/kotlin/net/corda/bridge/BridgeIntegrationTest.kt b/bridge/src/integration-test/kotlin/net/corda/bridge/BridgeIntegrationTest.kt
new file mode 100644
index 0000000000..0fea30482b
--- /dev/null
+++ b/bridge/src/integration-test/kotlin/net/corda/bridge/BridgeIntegrationTest.kt
@@ -0,0 +1,135 @@
+package net.corda.bridge
+
+import com.nhaarman.mockito_kotlin.doReturn
+import com.nhaarman.mockito_kotlin.whenever
+import net.corda.bridge.internal.BridgeInstance
+import net.corda.bridge.services.api.BridgeMode
+import net.corda.core.internal.div
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.node.services.config.CertChainPolicyConfig
+import net.corda.node.services.config.EnterpriseConfiguration
+import net.corda.node.services.config.MutualExclusionConfiguration
+import net.corda.node.services.config.NodeConfiguration
+import net.corda.node.services.messaging.ArtemisMessagingServer
+import net.corda.nodeapi.internal.ArtemisMessagingClient
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.DUMMY_BANK_A_NAME
+import net.corda.testing.core.MAX_MESSAGE_SIZE
+import net.corda.testing.core.SerializationEnvironmentRule
+import net.corda.testing.internal.rigorousMock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class BridgeIntegrationTest {
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ @Rule
+ @JvmField
+ val serializationEnvironment = SerializationEnvironmentRule(true)
+
+ private abstract class AbstractNodeConfiguration : NodeConfiguration
+
+ @Test
+ fun `Load simple all in one bridge and stand it up`() {
+ val configResource = "/net/corda/bridge/singleprocess/bridge.conf"
+ createNetworkParams(tempFolder.root.toPath())
+ val config = createAndLoadConfigFromResource(tempFolder.root.toPath(), configResource)
+ assertEquals(BridgeMode.SenderReceiver, config.bridgeMode)
+ assertEquals(NetworkHostAndPort("localhost", 11005), config.outboundConfig!!.artemisBrokerAddress)
+ assertEquals(NetworkHostAndPort("0.0.0.0", 10005), config.inboundConfig!!.listeningAddress)
+ assertNull(config.floatInnerConfig)
+ assertNull(config.floatOuterConfig)
+ config.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val (artemisServer, artemisClient) = createArtemis()
+ try {
+ val bridge = BridgeInstance(config, BridgeVersionInfo(1, "1.1", "Dummy", "Test"))
+ val stateFollower = bridge.activeChange.toBlocking().iterator
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, bridge.active)
+ bridge.start()
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, bridge.active)
+ assertEquals(true, serverListening("localhost", 10005))
+ bridge.stop()
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, bridge.active)
+ assertEquals(false, serverListening("localhost", 10005))
+ } finally {
+ artemisClient.stop()
+ artemisServer.stop()
+ }
+ }
+
+
+ @Test
+ fun `Load bridge (float inner) and float outer and stand them up`() {
+ val bridgeFolder = tempFolder.root.toPath()
+ val bridgeConfigResource = "/net/corda/bridge/withfloat/bridge/bridge.conf"
+ val bridgeConfig = createAndLoadConfigFromResource(bridgeFolder, bridgeConfigResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ createNetworkParams(bridgeFolder)
+ assertEquals(BridgeMode.FloatInner, bridgeConfig.bridgeMode)
+ assertEquals(NetworkHostAndPort("localhost", 11005), bridgeConfig.outboundConfig!!.artemisBrokerAddress)
+ val floatFolder = tempFolder.root.toPath() / "float"
+ val floatConfigResource = "/net/corda/bridge/withfloat/float/bridge.conf"
+ val floatConfig = createAndLoadConfigFromResource(floatFolder, floatConfigResource)
+ floatConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ createNetworkParams(floatFolder)
+ assertEquals(BridgeMode.FloatOuter, floatConfig.bridgeMode)
+ assertEquals(NetworkHostAndPort("0.0.0.0", 10005), floatConfig.inboundConfig!!.listeningAddress)
+ val (artemisServer, artemisClient) = createArtemis()
+ try {
+ val bridge = BridgeInstance(bridgeConfig, BridgeVersionInfo(1, "1.1", "Dummy", "Test"))
+ val bridgeStateFollower = bridge.activeChange.toBlocking().iterator
+ val float = BridgeInstance(floatConfig, BridgeVersionInfo(1, "1.1", "Dummy", "Test"))
+ val floatStateFollower = float.activeChange.toBlocking().iterator
+ assertEquals(false, floatStateFollower.next())
+ float.start()
+ assertEquals(true, floatStateFollower.next())
+ assertEquals(true, float.active) // float is running
+ assertEquals(false, serverListening("localhost", 10005)) // but not activated
+ assertEquals(false, bridgeStateFollower.next())
+ bridge.start()
+ assertEquals(true, bridgeStateFollower.next())
+ assertEquals(true, bridge.active)
+ assertEquals(true, float.active)
+ assertEquals(true, serverListening("localhost", 10005)) // now activated
+ bridge.stop()
+ assertEquals(false, bridgeStateFollower.next())
+ assertEquals(false, bridge.active)
+ assertEquals(true, float.active)
+ assertEquals(false, serverListening("localhost", 10005)) // now de-activated
+ float.stop()
+ assertEquals(false, floatStateFollower.next())
+ assertEquals(false, bridge.active)
+ assertEquals(false, float.active)
+ } finally {
+ artemisClient.stop()
+ artemisServer.stop()
+ }
+
+ }
+
+ private fun createArtemis(): Pair {
+ val artemisConfig = rigorousMock().also {
+ doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
+ doReturn(ALICE_NAME).whenever(it).myLegalName
+ doReturn("trustpass").whenever(it).trustStorePassword
+ doReturn("cordacadevpass").whenever(it).keyStorePassword
+ doReturn(NetworkHostAndPort("localhost", 11005)).whenever(it).p2pAddress
+ doReturn(null).whenever(it).jmxMonitoringHttpPort
+ doReturn(emptyList()).whenever(it).certificateChainCheckPolicies
+ doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000), externalBridge = true)).whenever(it).enterpriseConfiguration
+ }
+ val artemisServer = ArtemisMessagingServer(artemisConfig, 11005, MAX_MESSAGE_SIZE)
+ val artemisClient = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", 11005), MAX_MESSAGE_SIZE)
+ artemisServer.start()
+ artemisClient.start()
+ return Pair(artemisServer, artemisClient)
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/integration-test/kotlin/net/corda/bridge/services/AMQPListenerTest.kt b/bridge/src/integration-test/kotlin/net/corda/bridge/services/AMQPListenerTest.kt
new file mode 100644
index 0000000000..cf4ad93bec
--- /dev/null
+++ b/bridge/src/integration-test/kotlin/net/corda/bridge/services/AMQPListenerTest.kt
@@ -0,0 +1,166 @@
+package net.corda.bridge.services
+
+import net.corda.bridge.createAndLoadConfigFromResource
+import net.corda.bridge.createBridgeKeyStores
+import net.corda.bridge.createNetworkParams
+import net.corda.bridge.serverListening
+import net.corda.bridge.services.receiver.BridgeAMQPListenerServiceImpl
+import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.internal.div
+import net.corda.core.internal.readAll
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
+import net.corda.nodeapi.internal.crypto.X509KeyStore
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.DUMMY_BANK_A_NAME
+import net.corda.testing.core.DUMMY_BANK_B_NAME
+import net.corda.testing.core.SerializationEnvironmentRule
+import org.junit.Assert.assertArrayEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import kotlin.test.assertEquals
+
+class AMQPListenerTest {
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ @Rule
+ @JvmField
+ val serializationEnvironment = SerializationEnvironmentRule(true)
+
+ @Test
+ fun `Basic AMPQListenerService lifecycle test`() {
+ val configResource = "/net/corda/bridge/singleprocess/bridge.conf"
+ createNetworkParams(tempFolder.root.toPath())
+ val bridgeConfig = createAndLoadConfigFromResource(tempFolder.root.toPath() / "listener", configResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val auditService = TestAuditService()
+ val amqpListenerService = BridgeAMQPListenerServiceImpl(bridgeConfig, auditService)
+ val stateFollower = amqpListenerService.activeChange.toBlocking().iterator
+ val connectionFollower = amqpListenerService.onConnection.toBlocking().iterator
+ val auditFollower = auditService.onAuditEvent.toBlocking().iterator
+ // Listener doesn't come up yet as not started
+ assertEquals(false, stateFollower.next())
+ amqpListenerService.start()
+ // Listener still not up as audit not ready
+ assertEquals(false, amqpListenerService.active)
+ auditService.start()
+ // Service 'active', but no listening activity yet
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, amqpListenerService.active)
+ assertEquals(false, serverListening("localhost", 10005))
+ val keyStoreBytes = bridgeConfig.sslKeystore.readAll()
+ val trustStoreBytes = bridgeConfig.trustStoreFile.readAll()
+ // start listening
+ amqpListenerService.provisionKeysAndActivate(keyStoreBytes,
+ bridgeConfig.keyStorePassword.toCharArray(),
+ bridgeConfig.keyStorePassword.toCharArray(),
+ trustStoreBytes,
+ bridgeConfig.trustStorePassword.toCharArray())
+ // Fire lots of activity to prove we are good
+ assertEquals(TestAuditService.AuditEvent.STATUS_CHANGE, auditFollower.next())
+ assertEquals(true, amqpListenerService.active)
+ // Definitely a socket tehre
+ assertEquals(true, serverListening("localhost", 10005))
+ // But not a valid SSL link
+ assertEquals(false, connectionFollower.next().connected)
+ assertEquals(TestAuditService.AuditEvent.FAILED_CONNECTION, auditFollower.next())
+ val clientConfig = createAndLoadConfigFromResource(tempFolder.root.toPath() / "client", configResource)
+ clientConfig.createBridgeKeyStores(DUMMY_BANK_B_NAME)
+ val clientKeyStore = clientConfig.loadSslKeyStore().internal
+ val clientTrustStore = clientConfig.loadTrustStore().internal
+ // create and connect a real client
+ val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", 10005)),
+ setOf(DUMMY_BANK_A_NAME),
+ PEER_USER,
+ PEER_USER,
+ clientKeyStore,
+ clientConfig.keyStorePassword,
+ clientTrustStore)
+
+ amqpClient.start()
+ // Should see events to show we got a valid connection
+ val connectedEvent = connectionFollower.next()
+ assertEquals(true, connectedEvent.connected)
+ assertEquals(DUMMY_BANK_B_NAME, CordaX500Name.build(connectedEvent.remoteCert!!.subjectX500Principal))
+ assertEquals(TestAuditService.AuditEvent.SUCCESSFUL_CONNECTION, auditFollower.next())
+ val receiver = amqpListenerService.onReceive.toBlocking().iterator
+ // Send a test message
+ val testMsg = "A test".toByteArray()
+ val msg = amqpClient.createMessage(testMsg, "${PEERS_PREFIX}fake", DUMMY_BANK_A_NAME.toString(), emptyMap())
+ amqpClient.write(msg)
+ val receivedMessage = receiver.next()
+ // confirm details match
+ assertEquals(DUMMY_BANK_B_NAME, CordaX500Name.parse(receivedMessage.sourceLegalName))
+ assertArrayEquals(testMsg, receivedMessage.payload)
+ receivedMessage.complete(true)
+ assertEquals(MessageStatus.Acknowledged, msg.onComplete.get())
+ // Shutdown link
+ amqpClient.stop()
+ // verify audit events for disconnect
+ val disconnectedEvent = connectionFollower.next()
+ assertEquals(false, disconnectedEvent.connected)
+ assertEquals(DUMMY_BANK_B_NAME, CordaX500Name.build(disconnectedEvent.remoteCert!!.subjectX500Principal))
+ assertEquals(TestAuditService.AuditEvent.FAILED_CONNECTION, auditFollower.next())
+ // tear down listener
+ amqpListenerService.wipeKeysAndDeactivate()
+ assertEquals(true, amqpListenerService.active)
+ assertEquals(false, serverListening("localhost", 10005))
+ amqpListenerService.stop()
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, amqpListenerService.active)
+ }
+
+
+ @Test
+ fun `Bad certificate audit check`() {
+ val configResource = "/net/corda/bridge/singleprocess/bridge.conf"
+ createNetworkParams(tempFolder.root.toPath())
+ val bridgeConfig = createAndLoadConfigFromResource(tempFolder.root.toPath() / "listener", configResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val auditService = TestAuditService()
+ val amqpListenerService = BridgeAMQPListenerServiceImpl(bridgeConfig, auditService)
+ amqpListenerService.start()
+ auditService.start()
+ val keyStoreBytes = bridgeConfig.sslKeystore.readAll()
+ val trustStoreBytes = bridgeConfig.trustStoreFile.readAll()
+ // start listening
+ amqpListenerService.provisionKeysAndActivate(keyStoreBytes,
+ bridgeConfig.keyStorePassword.toCharArray(),
+ bridgeConfig.keyStorePassword.toCharArray(),
+ trustStoreBytes,
+ bridgeConfig.trustStorePassword.toCharArray())
+ val connectionFollower = amqpListenerService.onConnection.toBlocking().iterator
+ val auditFollower = auditService.onAuditEvent.toBlocking().iterator
+ val clientKeys = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256)
+ val clientCert = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, clientKeys)
+ val clientKeyStore = X509KeyStore("password")
+ clientKeyStore.setPrivateKey("TLS_CERT", clientKeys.private, listOf(clientCert))
+ val clientTrustStore = X509KeyStore("password")
+ clientTrustStore.setCertificate("TLS_ROOT", clientCert)
+ // create and connect a real client
+ val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", 10005)),
+ setOf(DUMMY_BANK_A_NAME),
+ PEER_USER,
+ PEER_USER,
+ clientKeyStore.internal,
+ "password",
+ clientTrustStore.internal)
+ amqpClient.start()
+ val connectionEvent = connectionFollower.next()
+ assertEquals(false, connectionEvent.connected)
+ assertEquals(TestAuditService.AuditEvent.FAILED_CONNECTION, auditFollower.next())
+ amqpClient.stop()
+ amqpListenerService.wipeKeysAndDeactivate()
+ amqpListenerService.stop()
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/integration-test/kotlin/net/corda/bridge/services/ArtemisConnectionTest.kt b/bridge/src/integration-test/kotlin/net/corda/bridge/services/ArtemisConnectionTest.kt
new file mode 100644
index 0000000000..810788ded4
--- /dev/null
+++ b/bridge/src/integration-test/kotlin/net/corda/bridge/services/ArtemisConnectionTest.kt
@@ -0,0 +1,103 @@
+package net.corda.bridge.services
+
+import com.nhaarman.mockito_kotlin.doReturn
+import com.nhaarman.mockito_kotlin.whenever
+import net.corda.bridge.createAndLoadConfigFromResource
+import net.corda.bridge.createBridgeKeyStores
+import net.corda.bridge.createNetworkParams
+import net.corda.bridge.services.artemis.BridgeArtemisConnectionServiceImpl
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.node.services.config.CertChainPolicyConfig
+import net.corda.node.services.config.EnterpriseConfiguration
+import net.corda.node.services.config.MutualExclusionConfiguration
+import net.corda.node.services.config.NodeConfiguration
+import net.corda.node.services.messaging.ArtemisMessagingServer
+import net.corda.testing.core.DUMMY_BANK_A_NAME
+import net.corda.testing.core.MAX_MESSAGE_SIZE
+import net.corda.testing.core.SerializationEnvironmentRule
+import net.corda.testing.internal.rigorousMock
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class ArtemisConnectionTest {
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ @Rule
+ @JvmField
+ val serializationEnvironment = SerializationEnvironmentRule(true)
+
+ private abstract class AbstractNodeConfiguration : NodeConfiguration
+
+ @Test
+ fun `Basic lifecycle test`() {
+ val configResource = "/net/corda/bridge/singleprocess/bridge.conf"
+ createNetworkParams(tempFolder.root.toPath())
+ val bridgeConfig = createAndLoadConfigFromResource(tempFolder.root.toPath(), configResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val auditService = TestAuditService()
+ val artemisService = BridgeArtemisConnectionServiceImpl(bridgeConfig, MAX_MESSAGE_SIZE, auditService)
+ val stateFollower = artemisService.activeChange.toBlocking().iterator
+ artemisService.start()
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, artemisService.active)
+ assertNull(artemisService.started)
+ auditService.start()
+ assertEquals(false, artemisService.active)
+ assertNull(artemisService.started)
+ var artemisServer = createArtemis()
+ try {
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, artemisService.active)
+ assertNotNull(artemisService.started)
+ auditService.stop()
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, artemisService.active)
+ assertNull(artemisService.started)
+ auditService.start()
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, artemisService.active)
+ assertNotNull(artemisService.started)
+ } finally {
+ artemisServer.stop()
+ }
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, artemisService.active)
+ assertNull(artemisService.started)
+ artemisServer = createArtemis()
+ try {
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, artemisService.active)
+ assertNotNull(artemisService.started)
+ } finally {
+ artemisServer.stop()
+ }
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, artemisService.active)
+ assertNull(artemisService.started)
+ artemisService.stop()
+ }
+
+
+ private fun createArtemis(): ArtemisMessagingServer {
+ val artemisConfig = rigorousMock().also {
+ doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
+ doReturn(DUMMY_BANK_A_NAME).whenever(it).myLegalName
+ doReturn("trustpass").whenever(it).trustStorePassword
+ doReturn("cordacadevpass").whenever(it).keyStorePassword
+ doReturn(NetworkHostAndPort("localhost", 11005)).whenever(it).p2pAddress
+ doReturn(null).whenever(it).jmxMonitoringHttpPort
+ doReturn(emptyList()).whenever(it).certificateChainCheckPolicies
+ doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000), externalBridge = true)).whenever(it).enterpriseConfiguration
+ }
+ val artemisServer = ArtemisMessagingServer(artemisConfig, 11005, MAX_MESSAGE_SIZE)
+ artemisServer.start()
+ return artemisServer
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/integration-test/kotlin/net/corda/bridge/services/TunnelControlTest.kt b/bridge/src/integration-test/kotlin/net/corda/bridge/services/TunnelControlTest.kt
new file mode 100644
index 0000000000..357a99e3fd
--- /dev/null
+++ b/bridge/src/integration-test/kotlin/net/corda/bridge/services/TunnelControlTest.kt
@@ -0,0 +1,251 @@
+package net.corda.bridge.services
+
+import com.nhaarman.mockito_kotlin.*
+import net.corda.bridge.*
+import net.corda.bridge.services.api.BridgeAMQPListenerService
+import net.corda.bridge.services.api.IncomingMessageFilterService
+import net.corda.bridge.services.ha.SingleInstanceMasterService
+import net.corda.bridge.services.receiver.FloatControlListenerService
+import net.corda.bridge.services.receiver.TunnelingBridgeReceiverService
+import net.corda.core.internal.createDirectories
+import net.corda.core.internal.div
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.nodeapi.internal.protonwrapper.netty.ConnectionChange
+import net.corda.testing.core.DUMMY_BANK_A_NAME
+import net.corda.testing.core.DUMMY_BANK_B_NAME
+import net.corda.testing.core.SerializationEnvironmentRule
+import net.corda.testing.internal.rigorousMock
+import org.junit.Assert.assertArrayEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import rx.Observable
+import rx.subjects.PublishSubject
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+
+class TunnelControlTest {
+ companion object {
+ val inboxTopic = "${P2P_PREFIX}test"
+ }
+
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ @Rule
+ @JvmField
+ val serializationEnvironment = SerializationEnvironmentRule(true)
+
+ private abstract class TestBridgeAMQPListenerService : BridgeAMQPListenerService, TestServiceBase() {
+ private var _running: Boolean = false
+ override val running: Boolean
+ get() = _running
+
+ override fun provisionKeysAndActivate(keyStoreBytes: ByteArray, keyStorePassword: CharArray, keyStorePrivateKeyPassword: CharArray, trustStoreBytes: ByteArray, trustStorePassword: CharArray) {
+ _running = true
+ }
+
+ override fun wipeKeysAndDeactivate() {
+ _running = false
+ }
+ }
+
+ private abstract class TestIncomingMessageFilterService : IncomingMessageFilterService, TestServiceBase()
+
+ @Test
+ fun `Basic tunnel life cycle test`() {
+ val bridgeConfigResource = "/net/corda/bridge/withfloat/bridge/bridge.conf"
+ val bridgePath = tempFolder.root.toPath() / "bridge"
+ bridgePath.createDirectories()
+ createNetworkParams(bridgePath)
+ val bridgeConfig = createAndLoadConfigFromResource(bridgePath, bridgeConfigResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val bridgeAuditService = TestAuditService()
+ val haService = SingleInstanceMasterService(bridgeConfig, bridgeAuditService)
+ val filterService = createPartialMock()
+ val bridgeProxiedReceiverService = TunnelingBridgeReceiverService(bridgeConfig, bridgeAuditService, haService, filterService)
+ val bridgeStateFollower = bridgeProxiedReceiverService.activeChange.toBlocking().iterator
+ bridgeProxiedReceiverService.start()
+ assertEquals(false, bridgeStateFollower.next())
+ assertEquals(false, bridgeProxiedReceiverService.active)
+ bridgeAuditService.start()
+ assertEquals(false, bridgeProxiedReceiverService.active)
+ filterService.start()
+ assertEquals(false, bridgeProxiedReceiverService.active)
+ haService.start()
+ assertEquals(false, bridgeProxiedReceiverService.active)
+
+ val floatConfigResource = "/net/corda/bridge/withfloat/float/bridge.conf"
+ val floatPath = tempFolder.root.toPath() / "float"
+ floatPath.createDirectories()
+ createNetworkParams(floatPath)
+ val floatConfig = createAndLoadConfigFromResource(floatPath, floatConfigResource)
+ floatConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+
+ val floatAuditService = TestAuditService()
+ val amqpListenerService = createPartialMock().also {
+ doReturn(Observable.never()).whenever(it).onConnection
+ doReturn(Observable.never()).whenever(it).onReceive
+ }
+ val floatControlListener = FloatControlListenerService(floatConfig, floatAuditService, amqpListenerService)
+ val floatStateFollower = floatControlListener.activeChange.toBlocking().iterator
+ assertEquals(false, floatStateFollower.next())
+ assertEquals(false, floatControlListener.active)
+ floatControlListener.start()
+ assertEquals(false, floatControlListener.active)
+ floatAuditService.start()
+ assertEquals(false, floatControlListener.active)
+ verify(amqpListenerService, times(0)).wipeKeysAndDeactivate()
+ verify(amqpListenerService, times(0)).provisionKeysAndActivate(any(), any(), any(), any(), any())
+ assertEquals(false, serverListening("localhost", 12005))
+ amqpListenerService.start()
+ assertEquals(true, floatStateFollower.next())
+ assertEquals(true, floatControlListener.active)
+ assertEquals(true, serverListening("localhost", 12005))
+
+ assertEquals(true, bridgeStateFollower.next())
+ assertEquals(true, bridgeProxiedReceiverService.active)
+ verify(amqpListenerService, times(0)).wipeKeysAndDeactivate()
+ verify(amqpListenerService, times(1)).provisionKeysAndActivate(any(), any(), any(), any(), any())
+
+ haService.stop()
+ assertEquals(false, bridgeStateFollower.next())
+ assertEquals(false, bridgeProxiedReceiverService.active)
+ assertEquals(true, floatControlListener.active)
+ verify(amqpListenerService, times(1)).wipeKeysAndDeactivate()
+ verify(amqpListenerService, times(1)).provisionKeysAndActivate(any(), any(), any(), any(), any())
+ assertEquals(true, serverListening("localhost", 12005))
+
+ haService.start()
+ assertEquals(true, bridgeStateFollower.next())
+ assertEquals(true, bridgeProxiedReceiverService.active)
+ assertEquals(true, floatControlListener.active)
+ verify(amqpListenerService, times(1)).wipeKeysAndDeactivate()
+ verify(amqpListenerService, times(2)).provisionKeysAndActivate(any(), any(), any(), any(), any())
+
+ floatControlListener.stop()
+ assertEquals(false, floatControlListener.active)
+ bridgeProxiedReceiverService.stop()
+ assertEquals(false, bridgeStateFollower.next())
+ assertEquals(false, bridgeProxiedReceiverService.active)
+ }
+
+ @Test
+ fun `Inbound message test`() {
+ val bridgeConfigResource = "/net/corda/bridge/withfloat/bridge/bridge.conf"
+ val bridgePath = tempFolder.root.toPath() / "bridge"
+ bridgePath.createDirectories()
+ createNetworkParams(bridgePath)
+ val bridgeConfig = createAndLoadConfigFromResource(bridgePath, bridgeConfigResource)
+ bridgeConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+ val bridgeAuditService = TestAuditService()
+ val haService = SingleInstanceMasterService(bridgeConfig, bridgeAuditService)
+ val forwardedMessages = PublishSubject.create()
+ val filterService = createPartialMock().also {
+ doAnswer {
+ val msg = it.arguments[0] as ReceivedMessage
+ forwardedMessages.onNext(msg)
+ Unit
+ }.whenever(it).sendMessageToLocalBroker(any())
+ }
+ val bridgeProxiedReceiverService = TunnelingBridgeReceiverService(bridgeConfig, bridgeAuditService, haService, filterService)
+ val bridgeStateFollower = bridgeProxiedReceiverService.activeChange.toBlocking().iterator
+ bridgeProxiedReceiverService.start()
+ bridgeAuditService.start()
+ filterService.start()
+ haService.start()
+ assertEquals(false, bridgeStateFollower.next())
+
+ val floatConfigResource = "/net/corda/bridge/withfloat/float/bridge.conf"
+ val floatPath = tempFolder.root.toPath() / "float"
+ floatPath.createDirectories()
+ createNetworkParams(floatPath)
+ val floatConfig = createAndLoadConfigFromResource(floatPath, floatConfigResource)
+ floatConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME)
+
+ val floatAuditService = TestAuditService()
+ val receiveObserver = PublishSubject.create()
+ val amqpListenerService = createPartialMock().also {
+ doReturn(Observable.never()).whenever(it).onConnection
+ doReturn(receiveObserver).whenever(it).onReceive
+ }
+ val floatControlListener = FloatControlListenerService(floatConfig, floatAuditService, amqpListenerService)
+ floatControlListener.start()
+ floatAuditService.start()
+ amqpListenerService.start()
+ assertEquals(true, bridgeStateFollower.next())
+
+ // Message flows back fine from float to bridge and is then forwarded to the filter service
+ val receiver = forwardedMessages.toBlocking().iterator
+ val testPayload = ByteArray(1) { 0x11 }
+ val receivedMessage = rigorousMock().also {
+ doNothing().whenever(it).complete(true) // ACK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(NetworkHostAndPort("localhost", 12345)).whenever(it).sourceLink
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(testPayload).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ doReturn(DUMMY_BANK_A_NAME.toString()).whenever(it).destinationLegalName
+ doReturn(NetworkHostAndPort("localhost", 6789)).whenever(it).destinationLink
+ }
+ receiveObserver.onNext(receivedMessage)
+ val messageReceived = receiver.next()
+ messageReceived.complete(true)
+ assertArrayEquals(testPayload, messageReceived.payload)
+ assertEquals(inboxTopic, messageReceived.topic)
+ assertEquals(DUMMY_BANK_B_NAME.toString(), messageReceived.sourceLegalName)
+
+ // Message NAK is propagated backwards
+ val testPayload2 = ByteArray(1) { 0x22 }
+ val ackLatch = CountDownLatch(1)
+ val receivedMessage2 = rigorousMock().also {
+ doAnswer {
+ ackLatch.countDown()
+ Unit
+ }.whenever(it).complete(false) // NAK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(NetworkHostAndPort("localhost", 12345)).whenever(it).sourceLink
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(testPayload2).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ doReturn(DUMMY_BANK_A_NAME.toString()).whenever(it).destinationLegalName
+ doReturn(NetworkHostAndPort("localhost", 6789)).whenever(it).destinationLink
+ }
+ receiveObserver.onNext(receivedMessage2)
+ val messageReceived2 = receiver.next()
+ messageReceived2.complete(false) // cause NAK to be called
+ assertArrayEquals(testPayload2, messageReceived2.payload)
+ assertEquals(inboxTopic, messageReceived2.topic)
+ assertEquals(DUMMY_BANK_B_NAME.toString(), messageReceived2.sourceLegalName)
+ ackLatch.await(1, TimeUnit.SECONDS)
+ verify(receivedMessage2, times(1)).complete(false)
+
+ // Message NAK if connection dies, without message acceptance
+ val ackLatch2 = CountDownLatch(1)
+ val receivedMessage3 = rigorousMock().also {
+ doAnswer {
+ ackLatch2.countDown()
+ Unit
+ }.whenever(it).complete(false) // NAK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(NetworkHostAndPort("localhost", 12345)).whenever(it).sourceLink
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(testPayload2).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ doReturn(DUMMY_BANK_A_NAME.toString()).whenever(it).destinationLegalName
+ doReturn(NetworkHostAndPort("localhost", 6789)).whenever(it).destinationLink
+ }
+ receiveObserver.onNext(receivedMessage3)
+ receiver.next() // wait message on bridge
+ bridgeProxiedReceiverService.stop() // drop control link
+ ackLatch.await(1, TimeUnit.SECONDS)
+ verify(receivedMessage3, times(1)).complete(false)
+
+ floatControlListener.stop()
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/Bridge.kt b/bridge/src/main/kotlin/net/corda/bridge/Bridge.kt
new file mode 100644
index 0000000000..a9cb0632b5
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/Bridge.kt
@@ -0,0 +1,10 @@
+@file:JvmName("Bridge")
+
+package net.corda.bridge
+
+import net.corda.bridge.internal.BridgeStartup
+import kotlin.system.exitProcess
+
+fun main(args: Array) {
+ exitProcess(if (BridgeStartup(args).run()) 0 else 1)
+}
diff --git a/bridge/src/main/kotlin/net/corda/bridge/BridgeArgsParser.kt b/bridge/src/main/kotlin/net/corda/bridge/BridgeArgsParser.kt
new file mode 100644
index 0000000000..20049eded8
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/BridgeArgsParser.kt
@@ -0,0 +1,68 @@
+package net.corda.bridge
+
+import joptsimple.OptionParser
+import joptsimple.util.EnumConverter
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.config.BridgeConfigHelper
+import net.corda.bridge.services.config.parseAsBridgeConfiguration
+import net.corda.core.internal.div
+import org.slf4j.event.Level
+import java.io.PrintStream
+import java.nio.file.Path
+import java.nio.file.Paths
+
+// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
+class ArgsParser {
+ private val optionParser = OptionParser()
+ // The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
+ // Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
+ private val baseDirectoryArg = optionParser
+ .accepts("base-directory", "The bridge working directory where all the files are kept")
+ .withRequiredArg()
+ .defaultsTo(".")
+ private val configFileArg = optionParser
+ .accepts("config-file", "The path to the config file")
+ .withRequiredArg()
+ .defaultsTo("bridge.conf")
+ private val loggerLevel = optionParser
+ .accepts("logging-level", "Enable logging at this level and higher")
+ .withRequiredArg()
+ .withValuesConvertedBy(object : EnumConverter(Level::class.java) {})
+ .defaultsTo(Level.INFO)
+ private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
+ private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
+ private val helpArg = optionParser.accepts("help").forHelp()
+
+ fun parse(vararg args: String): CmdLineOptions {
+ val optionSet = optionParser.parse(*args)
+ require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) {
+ "${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together"
+ }
+ val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath()
+ val configFile = baseDirectory / optionSet.valueOf(configFileArg)
+ val help = optionSet.has(helpArg)
+ val loggingLevel = optionSet.valueOf(loggerLevel)
+ val logToConsole = optionSet.has(logToConsoleArg)
+ val isVersion = optionSet.has(isVersionArg)
+ return CmdLineOptions(baseDirectory,
+ configFile,
+ help,
+ loggingLevel,
+ logToConsole,
+ isVersion)
+ }
+
+ fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
+}
+
+data class CmdLineOptions(val baseDirectory: Path,
+ val configFile: Path,
+ val help: Boolean,
+ val loggingLevel: Level,
+ val logToConsole: Boolean,
+ val isVersion: Boolean) {
+ fun loadConfig(): BridgeConfiguration {
+ val config = BridgeConfigHelper.loadConfig(baseDirectory, configFile).parseAsBridgeConfiguration()
+ return config
+ }
+}
diff --git a/bridge/src/main/kotlin/net/corda/bridge/BridgeVersionInfo.kt b/bridge/src/main/kotlin/net/corda/bridge/BridgeVersionInfo.kt
new file mode 100644
index 0000000000..5150183c8a
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/BridgeVersionInfo.kt
@@ -0,0 +1,18 @@
+package net.corda.bridge
+
+
+/**
+ * Encapsulates various pieces of version information of the bridge.
+ */
+data class BridgeVersionInfo(
+ /**
+ * Platform version of the bridge which is an integer value which increments on any release where any of the public
+ * API of the entire Corda platform changes. This includes messaging, serialisation, bridge APIs, etc.
+ */
+ val platformVersion: Int,
+ /** Release version string of the bridge. */
+ val releaseVersion: String,
+ /** The exact version control commit ID of the bridge build. */
+ val revision: String,
+ /** The bridge vendor */
+ val vendor: String)
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeInstance.kt b/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeInstance.kt
new file mode 100644
index 0000000000..9523e04244
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeInstance.kt
@@ -0,0 +1,173 @@
+package net.corda.bridge.internal
+
+import net.corda.bridge.BridgeVersionInfo
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.audit.LoggingBridgeAuditService
+import net.corda.bridge.services.supervisors.BridgeSupervisorServiceImpl
+import net.corda.bridge.services.supervisors.FloatSupervisorServiceImpl
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.concurrent.CordaFuture
+import net.corda.core.internal.SignedDataWithCert
+import net.corda.core.internal.concurrent.openFuture
+import net.corda.core.internal.div
+import net.corda.core.internal.exists
+import net.corda.core.internal.readObject
+import net.corda.core.node.NetworkParameters
+import net.corda.core.serialization.internal.SerializationEnvironmentImpl
+import net.corda.core.serialization.internal.effectiveSerializationEnv
+import net.corda.core.serialization.internal.nodeSerializationEnv
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.ShutdownHook
+import net.corda.nodeapi.internal.addShutdownHook
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
+import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
+import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
+import net.corda.nodeapi.internal.serialization.AMQP_STORAGE_CONTEXT
+import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
+import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
+import rx.Subscription
+import java.util.concurrent.atomic.AtomicBoolean
+
+class BridgeInstance(val conf: BridgeConfiguration,
+ val versionInfo: BridgeVersionInfo,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : ServiceLifecycleSupport, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val shutdown = AtomicBoolean(false)
+ private var shutdownHook: ShutdownHook? = null
+
+ private lateinit var networkParameters: NetworkParameters
+ private lateinit var bridgeAuditService: BridgeAuditService
+ private var bridgeSupervisorService: BridgeSupervisorService? = null
+ private var floatSupervisorService: FloatSupervisorService? = null
+ private var statusFollower: ServiceStateCombiner? = null
+ private var statusSubscriber: Subscription? = null
+
+
+ init {
+ initialiseSerialization()
+ }
+
+ private fun initialiseSerialization() {
+ val serializationExists = try {
+ effectiveSerializationEnv
+ true
+ } catch (e: IllegalStateException) {
+ false
+ }
+ if (!serializationExists) {
+ val classloader = this.javaClass.classLoader
+ nodeSerializationEnv = SerializationEnvironmentImpl(
+ SerializationFactoryImpl().apply {
+ registerScheme(AMQPServerSerializationScheme(emptyList()))
+ },
+ p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
+ rpcServerContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
+ storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
+ checkpointContext = AMQP_P2P_CONTEXT.withClassLoader(classloader))
+ }
+ }
+
+ override fun start() {
+ val wasRunning = shutdown.getAndSet(true)
+ require(!wasRunning) { "Already running" }
+ shutdownHook = addShutdownHook {
+ stop()
+ }
+ retrieveNetworkParameters()
+ createServices()
+ startServices()
+ }
+
+ override fun stop() {
+ val wasRunning = shutdown.getAndSet(false)
+ if (!wasRunning) {
+ return
+ }
+ shutdownHook?.cancel()
+ shutdownHook = null
+ log.info("Shutting down ...")
+
+ stopServices()
+
+ _exitFuture.set(this)
+ log.info("Shutdown complete")
+ }
+
+ private val _exitFuture = openFuture()
+ val onExit: CordaFuture get() = _exitFuture
+
+ private fun retrieveNetworkParameters() {
+ val trustRoot = conf.loadTrustStore().getCertificate(X509Utilities.CORDA_ROOT_CA)
+ val networkParamsFile = conf.baseDirectory / NETWORK_PARAMS_FILE_NAME
+ require(networkParamsFile.exists()) { "No network-parameters file found." }
+ networkParameters = networkParamsFile.readObject>().verifiedNetworkMapCert(trustRoot)
+ log.info("Loaded network parameters: $networkParameters")
+ check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) {
+ "Node's platform version is lower than network's required minimumPlatformVersion"
+ }
+ }
+
+ private fun createServices() {
+ bridgeAuditService = LoggingBridgeAuditService(conf)
+ when (conf.bridgeMode) {
+ // In the SenderReceiver mode the inbound and outbound message paths are run from within a single bridge process.
+ // The process thus contains components that listen for bridge control messages on Artemis.
+ // The process can then initiates TLS/AMQP 1.0 connections to remote peers and transfers the outbound messages.
+ // The process also runs a TLS/AMQP 1.0 server socket, which is can receive connections and messages from peers,
+ // validate the messages and then forwards the packets to the Artemis inbox queue of the node.
+ BridgeMode.SenderReceiver -> {
+ floatSupervisorService = FloatSupervisorServiceImpl(conf, networkParameters.maxMessageSize, bridgeAuditService)
+ bridgeSupervisorService = BridgeSupervisorServiceImpl(conf, networkParameters.maxMessageSize, bridgeAuditService, floatSupervisorService!!.amqpListenerService)
+ }
+ // In the FloatInner mode the process runs the full outbound message path as in the SenderReceiver mode, but the inbound path is split.
+ // This 'Float Inner/Bridge Controller' process runs the more trusted portion of the inbound path.
+ // In particular the 'Float Inner/Bridge Controller' has access to the persisted TLS KeyStore, which it provisions dynamically into the 'Float Outer'.
+ // Also the the 'Float Inner' does more complete validation of inbound messages and ensures that they correspond to legitimate
+ // node inboxes, before transferring the message to Artemis. Potentially it might carry out deeper checks of received packets.
+ // However, the 'Float Inner' is not directly exposed to the internet, or peers and does not host the TLS/AMQP 1.0 server socket.
+ BridgeMode.FloatInner -> {
+ bridgeSupervisorService = BridgeSupervisorServiceImpl(conf, networkParameters.maxMessageSize, bridgeAuditService, null)
+ }
+ // In the FloatOuter mode this process runs a minimal AMQP proxy that is designed to run in a DMZ zone.
+ // The process holds the minimum data necessary to act as the TLS/AMQP 1.0 receiver socket and tries
+ // to minimise any state. It specifically does not persist the Node TLS keys anywhere, nor does it hold network map information on peers.
+ // The 'Float Outer' does not initiate socket connection anywhere, so that attackers can be easily blocked by firewalls
+ // if they try to invade the system from a compromised 'Float Outer' machine. The 'Float Outer' hosts a control TLS/AMQP 1.0 server socket,
+ // which receives a connection from the 'Float Inner/Bridge controller' in the trusted zone of the organisation.
+ // The control channel is ideally authenticated using server/client certificates that are not related to the Corda PKI hierarchy.
+ // Once the control channel is formed it is used to RPC the methods of the BridgeAMQPListenerService to start the publicly visible
+ // TLS/AMQP 1.0 server socket of the Corda node. Thus peer connections will directly terminate onto the activate listener socket and
+ // be validated against the keys/certificates sent across the control tunnel. Inbound messages are given basic checks that do not require
+ // holding potentially sensitive information and are then forwarded across the control tunnel to the 'Float Inner' process for more
+ // complete validation checks.
+ BridgeMode.FloatOuter -> {
+ floatSupervisorService = FloatSupervisorServiceImpl(conf, networkParameters.maxMessageSize, bridgeAuditService)
+ }
+ }
+ statusFollower = ServiceStateCombiner(listOf(bridgeAuditService, floatSupervisorService, bridgeSupervisorService).filterNotNull())
+ statusSubscriber = statusFollower!!.activeChange.subscribe {
+ stateHelper.active = it
+ }
+ }
+
+ private fun startServices() {
+ bridgeAuditService.start()
+ bridgeSupervisorService?.start()
+ floatSupervisorService?.start()
+ }
+
+ private fun stopServices() {
+ stateHelper.active = false
+ floatSupervisorService?.stop()
+ bridgeSupervisorService?.stop()
+ bridgeAuditService.stop()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ statusFollower = null
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeStartup.kt b/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeStartup.kt
new file mode 100644
index 0000000000..78c88e0156
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/internal/BridgeStartup.kt
@@ -0,0 +1,204 @@
+package net.corda.bridge.internal
+
+import com.jcabi.manifests.Manifests
+import joptsimple.OptionException
+import net.corda.bridge.ArgsParser
+import net.corda.bridge.BridgeVersionInfo
+import net.corda.bridge.CmdLineOptions
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.core.internal.createDirectories
+import net.corda.core.internal.div
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.addShutdownHook
+import org.slf4j.bridge.SLF4JBridgeHandler
+import sun.misc.VMSupport
+import java.io.RandomAccessFile
+import java.lang.management.ManagementFactory
+import java.net.InetAddress
+import java.nio.file.Path
+import java.util.*
+import kotlin.system.exitProcess
+
+class BridgeStartup(val args: Array) {
+ companion object {
+ // lazy init the logging, because the logging levels aren't configured until we have parsed some options.
+ private val log by lazy { contextLogger() }
+ val LOGS_DIRECTORY_NAME = "logs"
+ }
+
+ /**
+ * @return true if the bridge startup was successful. This value is intended to be the exit code of the process.
+ */
+ fun run(): Boolean {
+ val startTime = System.currentTimeMillis()
+ val (argsParser, cmdlineOptions) = parseArguments()
+
+ // We do the single bridge check before we initialise logging so that in case of a double-bridge start it
+ // doesn't mess with the running bridge's logs.
+ enforceSingleBridgeIsRunning(cmdlineOptions.baseDirectory)
+
+ initLogging(cmdlineOptions)
+
+ val versionInfo = getVersionInfo()
+
+ if (cmdlineOptions.isVersion) {
+ println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
+ println("Revision ${versionInfo.revision}")
+ println("Platform Version ${versionInfo.platformVersion}")
+ return true
+ }
+
+ // Maybe render command line help.
+ if (cmdlineOptions.help) {
+ argsParser.printHelp(System.out)
+ return true
+ }
+ val conf = try {
+ loadConfigFile(cmdlineOptions)
+ } catch (e: Exception) {
+ log.error("Exception during bridge configuration", e)
+ return false
+ }
+
+ try {
+ logStartupInfo(versionInfo, cmdlineOptions, conf)
+ } catch (e: Exception) {
+ log.error("Exception during bridge registration", e)
+ return false
+ }
+
+ val bridge = try {
+ cmdlineOptions.baseDirectory.createDirectories()
+ startBridge(conf, versionInfo, startTime)
+ } catch (e: Exception) {
+ if (e.message?.startsWith("Unknown named curve:") == true) {
+ log.error("Exception during bridge startup - ${e.message}. " +
+ "This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
+ } else {
+ log.error("Exception during bridge startup", e)
+ }
+ return false
+ }
+
+ if (System.getProperties().containsKey("WAIT_KEY_FOR_EXIT")) {
+ System.`in`.read() // Inside IntelliJ we can't forward CTRL-C, so debugging shutdown is a nightmare. So allow -DWAIT_KEY_FOR_EXIT flag for key based quit.
+ } else {
+ bridge.onExit.get()
+ }
+
+ log.info("bridge shutting down")
+ bridge.stop()
+
+ return true
+ }
+
+ fun logStartupInfo(versionInfo: BridgeVersionInfo, cmdlineOptions: CmdLineOptions, conf: BridgeConfiguration) {
+ log.info("Vendor: ${versionInfo.vendor}")
+ log.info("Release: ${versionInfo.releaseVersion}")
+ log.info("Platform Version: ${versionInfo.platformVersion}")
+ log.info("Revision: ${versionInfo.revision}")
+ val info = ManagementFactory.getRuntimeMXBean()
+ log.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this
+ log.info("Main class: ${BridgeStartup::class.java.protectionDomain.codeSource.location.toURI().path}")
+ log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
+ log.info("Application Args: ${args.joinToString(" ")}")
+ log.info("bootclasspath: ${info.bootClassPath}")
+ log.info("classpath: ${info.classPath}")
+ log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
+ log.info("Machine: ${lookupMachineNameAndMaybeWarn()}")
+ log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
+ val agentProperties = VMSupport.getAgentProperties()
+ if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
+ log.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
+ }
+ log.info("Starting as bridge mode of ${conf.bridgeMode}")
+ }
+
+ protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): BridgeConfiguration = cmdlineOptions.loadConfig()
+
+ protected fun getVersionInfo(): BridgeVersionInfo {
+ // Manifest properties are only available if running from the corda jar
+ fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
+
+ return BridgeVersionInfo(
+ manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
+ manifestValue("Corda-Release-Version") ?: "Unknown",
+ manifestValue("Corda-Revision") ?: "Unknown",
+ manifestValue("Corda-Vendor") ?: "Unknown"
+ )
+ }
+
+ private fun enforceSingleBridgeIsRunning(baseDirectory: Path) {
+ // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
+ // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
+ // exists, we try to take the file lock first before replacing it and if that fails it means we're being started
+ // twice with the same directory: that's a user error and we should bail out.
+ val pidFile = (baseDirectory / "bridge-process-id").toFile()
+ pidFile.createNewFile()
+ pidFile.deleteOnExit()
+ val pidFileRw = RandomAccessFile(pidFile, "rw")
+ val pidFileLock = pidFileRw.channel.tryLock()
+ if (pidFileLock == null) {
+ println("It appears there is already a bridge running with the specified data directory $baseDirectory")
+ println("Shut that other bridge down and try again. It may have process ID ${pidFile.readText()}")
+ System.exit(1)
+ }
+ // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
+ // when our process shuts down, but we try in stop() anyway just to be nice.
+ addShutdownHook {
+ pidFileLock.release()
+ }
+ val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0]
+ pidFileRw.setLength(0)
+ pidFileRw.write(ourProcessID.toByteArray())
+ }
+
+ private fun lookupMachineNameAndMaybeWarn(): String {
+ val start = System.currentTimeMillis()
+ val hostName: String = InetAddress.getLocalHost().hostName
+ val elapsed = System.currentTimeMillis() - start
+ if (elapsed > 1000 && hostName.endsWith(".local")) {
+ // User is probably on macOS and experiencing this problem: http://stackoverflow.com/questions/10064581/how-can-i-eliminate-slow-resolving-loading-of-localhost-virtualhost-a-2-3-secon
+ //
+ // Also see https://bugs.openjdk.java.net/browse/JDK-8143378
+ val messages = listOf(
+ "Your computer took over a second to resolve localhost due an incorrect configuration. Corda will work but start very slowly until this is fixed. ",
+ "Please see https://docs.corda.net/troubleshooting.html#slow-localhost-resolution for information on how to fix this. ",
+ "It will only take a few seconds for you to resolve."
+ )
+ log.warn(messages.joinToString(""))
+ }
+ return hostName
+ }
+
+ private fun parseArguments(): Pair {
+ val argsParser = ArgsParser()
+ val cmdlineOptions = try {
+ argsParser.parse(*args)
+ } catch (ex: OptionException) {
+ println("Invalid command line arguments: ${ex.message}")
+ argsParser.printHelp(System.out)
+ exitProcess(1)
+ }
+ return Pair(argsParser, cmdlineOptions)
+ }
+
+ fun initLogging(cmdlineOptions: CmdLineOptions) {
+ val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
+ System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
+ if (cmdlineOptions.logToConsole) {
+ System.setProperty("consoleLogLevel", loggingLevel)
+ }
+ System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
+ SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
+ SLF4JBridgeHandler.install()
+ }
+
+ fun startBridge(conf: BridgeConfiguration, versionInfo: BridgeVersionInfo, startTime: Long): BridgeInstance {
+ val bridge = BridgeInstance(conf, versionInfo)
+ bridge.start()
+ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
+ log.info("Bridge started up and registered in $elapsed sec")
+ return bridge
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAMQPListenerService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAMQPListenerService.kt
new file mode 100644
index 0000000000..961e1bd79a
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAMQPListenerService.kt
@@ -0,0 +1,45 @@
+package net.corda.bridge.services.api
+
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.nodeapi.internal.protonwrapper.netty.ConnectionChange
+import rx.Observable
+import java.security.KeyStore
+
+/**
+ * This service when activated via [provisionKeysAndActivate] installs an AMQP listening socket,
+ * which listens on the port specified in the [BridgeConfiguration.inboundConfig] section.
+ * The service technically runs inside the 'float' portion of the bridge, so that it can be run remotely inside the DMZ.
+ * As a result it reports as active, whilst not actually listening. Only when the TLS [KeyStore]s are passed to it
+ * does the service become [running].
+ */
+interface BridgeAMQPListenerService : ServiceLifecycleSupport {
+ /**
+ * Passes in the [KeyStore]s containing the TLS keys and certificates. This data is only to be held in memory
+ * and will be wiped on close.
+ */
+ fun provisionKeysAndActivate(keyStoreBytes: ByteArray,
+ keyStorePassword: CharArray,
+ keyStorePrivateKeyPassword: CharArray,
+ trustStoreBytes: ByteArray,
+ trustStorePassword: CharArray)
+
+ /**
+ * Stop listening on the socket and cleanup any private data/keys.
+ */
+ fun wipeKeysAndDeactivate()
+
+ /**
+ * If the service is [running] the AMQP listener is active.
+ */
+ val running: Boolean
+
+ /**
+ * Incoming AMQP packets from remote peers are available on this [Observable].
+ */
+ val onReceive: Observable
+
+ /**
+ * Any connection, disconnection, or authentication failure is available on this [Observable].
+ */
+ val onConnection: Observable
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeArtemisConnectionService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeArtemisConnectionService.kt
new file mode 100644
index 0000000000..cc75cc873f
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeArtemisConnectionService.kt
@@ -0,0 +1,14 @@
+package net.corda.bridge.services.api
+
+import net.corda.nodeapi.internal.ArtemisMessagingClient
+
+/**
+ * This provides a service to manage connection to the local broker as defined in the [BridgeConfiguration.outboundConfig] section.
+ * Once started the service will repeatedly attempt to connect to the bus, signalling success by changing to the [active] state.
+ */
+interface BridgeArtemisConnectionService : ServiceLifecycleSupport {
+ /**
+ * When the service becomes [active] this will be non-null and provides access to Artemis management objects.
+ */
+ val started: ArtemisMessagingClient.Started?
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAuditService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAuditService.kt
new file mode 100644
index 0000000000..67dbd13e8a
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeAuditService.kt
@@ -0,0 +1,17 @@
+package net.corda.bridge.services.api
+
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import java.net.InetSocketAddress
+
+/**
+ * This service provides centralised facilities for recording business critical events in the bridge.
+ * Currently the simple implementation just records events to log file, but future implementations may need to post
+ * security data to an enterprise service.
+ */
+interface BridgeAuditService : ServiceLifecycleSupport {
+ fun successfulConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress, certificateSubject: String, msg: String)
+ fun failedConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress?, certificateSubject: String?, msg: String)
+ fun packetDropEvent(packet: ReceivedMessage?, msg: String)
+ fun packetAcceptedEvent(packet: ReceivedMessage)
+ fun statusChangeEvent(msg: String)
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeConfiguration.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeConfiguration.kt
new file mode 100644
index 0000000000..7be9e2758f
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeConfiguration.kt
@@ -0,0 +1,92 @@
+package net.corda.bridge.services.api
+
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.config.NodeSSLConfiguration
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import java.nio.file.Path
+
+enum class BridgeMode {
+ /**
+ * The Bridge/Float is run as a single process with both AMQP sending and receiving functionality.
+ */
+ SenderReceiver,
+ /**
+ * Runs only the trusted bridge side of the system, which has direct TLS access to Artemis.
+ * The components handles all outgoing aspects of AMQP bridges directly.
+ * The inbound messages are initially received onto a different [FloatOuter] process and a
+ * separate AMQP tunnel is used to ship back the inbound data to this [FloatInner] process.
+ */
+ FloatInner,
+ /**
+ * A minimal process designed to be run inside a DMZ, which acts an AMQP receiver of inbound peer messages.
+ * The component carries out basic validation of the TLS sources and AMQP packets, before forwarding to the [FloatInner].
+ * No keys are stored on disk for the component, but must instead be provisioned from the [FloatInner] using a
+ * separate AMQP link initiated from the [FloatInner] to the [FloatOuter].
+ */
+ FloatOuter
+}
+
+/**
+ * Details of the local Artemis broker.
+ * Required in SenderReceiver and FloatInner modes.
+ */
+interface BridgeOutboundConfiguration {
+ val artemisBrokerAddress: NetworkHostAndPort
+ // Allows override of [KeyStore] details for the artemis connection, otherwise the general top level details are used.
+ val customSSLConfiguration: SSLConfiguration?
+}
+
+/**
+ * Details of the inbound socket binding address, which should be where external peers
+ * using the node's network map advertised data should route links and directly terminate their TLS connections.
+ * This configuration is required in SenderReceiver and FloatOuter modes.
+ */
+interface BridgeInboundConfiguration {
+ val listeningAddress: NetworkHostAndPort
+ // Allows override of [KeyStore] details for the AMQP listener port, otherwise the general top level details are used.
+ val customSSLConfiguration: SSLConfiguration?
+}
+
+/**
+ * Details of the target control ports of available [BridgeMode.FloatOuter] processes from the perspective of the [BridgeMode.FloatInner] process.
+ * Required for [BridgeMode.FloatInner] mode.
+ */
+interface FloatInnerConfiguration {
+ val floatAddresses: List
+ val expectedCertificateSubject: CordaX500Name
+ // Allows override of [KeyStore] details for the control port, otherwise the general top level details are used.
+ // Used for connection to Float in DMZ
+ val customSSLConfiguration: SSLConfiguration?
+ // The SSL keystores to provision into the Float in DMZ
+ val customFloatOuterSSLConfiguration: SSLConfiguration?
+}
+
+/**
+ * Details of the listening port for a [BridgeMode.FloatOuter] process and of the certificate that the [BridgeMode.FloatInner] should present.
+ * Required for [BridgeMode.FloatOuter] mode.
+ */
+interface FloatOuterConfiguration {
+ val floatAddress: NetworkHostAndPort
+ val expectedCertificateSubject: CordaX500Name
+ // Allows override of [KeyStore] details for the control port, otherwise the general top level details are used.
+ val customSSLConfiguration: SSLConfiguration?
+}
+
+interface BridgeConfiguration : NodeSSLConfiguration {
+ val bridgeMode: BridgeMode
+ val outboundConfig: BridgeOutboundConfiguration?
+ val inboundConfig: BridgeInboundConfiguration?
+ val floatInnerConfig: FloatInnerConfiguration?
+ val floatOuterConfig: FloatOuterConfiguration?
+ val haConfig: String?
+ val networkParametersPath: Path
+ val enableAMQPPacketTrace: Boolean
+ // Reconnect to artemis after [artemisReconnectionInterval] ms the default value is 5000 ms.
+ val artemisReconnectionInterval: Int
+ // The period to wait for clean shutdown of remote components
+ // e.g links to the Float Outer, or Artemis sessions, before the process continues shutting down anyway.
+ // Default value is 1000 ms.
+ val politeShutdownPeriod: Int
+ val whitelistedHeaders: List
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeMasterService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeMasterService.kt
new file mode 100644
index 0000000000..efe6df9e88
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeMasterService.kt
@@ -0,0 +1,11 @@
+package net.corda.bridge.services.api
+
+/**
+ * This service controls when a bridge may become active and start relaying messages to/from the artemis broker.
+ * The active flag is the used to gate dependent services, which should hold off connecting to the bus until this service
+ * has been able to become active.
+ */
+interface BridgeMasterService : ServiceLifecycleSupport {
+ // An echo of the active flag that can be used to make the intention of active status checks clearer.
+ val isMaster: Boolean get() = active
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeReceiverService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeReceiverService.kt
new file mode 100644
index 0000000000..88c03f3797
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeReceiverService.kt
@@ -0,0 +1,10 @@
+package net.corda.bridge.services.api
+
+/**
+ * The [BridgeReceiverService] is the service responsible for joining together the perhaps remote [BridgeAMQPListenerService]
+ * and the outgoing [IncomingMessageFilterService] that provides the validation and filtering path into the local Artemis broker.
+ * It should not become active, or transmit messages until all of the dependencies are themselves active.
+ */
+interface BridgeReceiverService : ServiceLifecycleSupport {
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSenderService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSenderService.kt
new file mode 100644
index 0000000000..a06cf16e2c
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSenderService.kt
@@ -0,0 +1,19 @@
+package net.corda.bridge.services.api
+
+import net.corda.core.identity.CordaX500Name
+import net.corda.nodeapi.internal.bridging.BridgeControlListener
+
+/**
+ * This service is responsible for the outgoing path of messages from the local Artemis broker
+ * to the remote peer using AMQP. It should not become active until the connection to the local Artemis broker is stable
+ * and the [BridgeMasterService] has allowed this bridge instance to become activated.
+ * In practice the actual AMQP bridging logic is carried out using an instance of the [BridgeControlListener] class with
+ * lifecycle support coming from the service.
+ */
+interface BridgeSenderService : ServiceLifecycleSupport {
+ /**
+ * This method is used to check inbound packets against the list of valid inbox addresses registered from the nodes
+ * via the local Bridge Control Protocol. They may optionally also check this against the source legal name.
+ */
+ fun validateReceiveTopic(topic: String, sourceLegalName: CordaX500Name): Boolean
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSupervisorService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSupervisorService.kt
new file mode 100644
index 0000000000..f661907e71
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/BridgeSupervisorService.kt
@@ -0,0 +1,9 @@
+package net.corda.bridge.services.api
+
+/**
+ * This is the top level service representing the [BridgeMode.FloatInner] service stack. The primary role of this component is to
+ * create and wire up concrete implementations of the relevant services according to the [BridgeConfiguration] details.
+ * The possibly proxied path to the [BridgeAMQPListenerService] is typically a constructor input
+ * as that is a [BridgeMode.FloatOuter] component.
+ */
+interface BridgeSupervisorService : ServiceLifecycleSupport
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatControlService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatControlService.kt
new file mode 100644
index 0000000000..8706495930
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatControlService.kt
@@ -0,0 +1,7 @@
+package net.corda.bridge.services.api
+
+/**
+ * This service represent an AMQP socket listener that awaits a remote initiated connection from the [BridgeMode.FloatInner].
+ * Only one active connection is allowed at a time and it must match the configured requirements in the [BridgeConfiguration.floatInnerConfig].
+ */
+interface FloatControlService : ServiceLifecycleSupport
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatSupervisorService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatSupervisorService.kt
new file mode 100644
index 0000000000..ff45bf0908
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/FloatSupervisorService.kt
@@ -0,0 +1,10 @@
+package net.corda.bridge.services.api
+
+/**
+ * This is the top level service responsible for creating and managing the [BridgeMode.FloatOuter] portions of the bridge.
+ * It exposes a possibly proxied [BridgeAMQPListenerService] component that is used in the [BridgeSupervisorService]
+ * to wire up the internal portions of the AMQP peer inbound message path.
+ */
+interface FloatSupervisorService : ServiceLifecycleSupport {
+ val amqpListenerService: BridgeAMQPListenerService
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/IncomingMessageFilterService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/IncomingMessageFilterService.kt
new file mode 100644
index 0000000000..a515bb52bc
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/IncomingMessageFilterService.kt
@@ -0,0 +1,13 @@
+package net.corda.bridge.services.api
+
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+
+/**
+ * This service is responsible for security checking the incoming packets to ensure they are for a legitimate node inbox and
+ * potentially for any other security related aspects. If the message is badly formed then it will be dropped and an audit event logged.
+ * Otherwise the message is forwarded to the appropriate node inbox on the local Artemis Broker.
+ * The service will not be active until the underlying [BridgeArtemisConnectionService] is active.
+ */
+interface IncomingMessageFilterService : ServiceLifecycleSupport {
+ fun sendMessageToLocalBroker(inboundMessage: ReceivedMessage)
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/api/ServiceLifecycleSupport.kt b/bridge/src/main/kotlin/net/corda/bridge/services/api/ServiceLifecycleSupport.kt
new file mode 100644
index 0000000000..3cfc730b13
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/api/ServiceLifecycleSupport.kt
@@ -0,0 +1,43 @@
+package net.corda.bridge.services.api
+
+import rx.Observable
+
+/**
+ * Basic interface to represent the dynamic life cycles of services that may be running, but may have to await external dependencies,
+ * or for HA master state.
+ * Implementations of this should be implemented in a thread safe fashion.
+ */
+interface ServiceStateSupport {
+ /**
+ * Reads the current dynamic status of the service, which should only become true after the service has been started,
+ * any dynamic resources have been started/registered and any network connections have been completed.
+ * Failure to acquire a resource, or manual stop of the service, should return this to false.
+ */
+ val active: Boolean
+
+ /**
+ * This Observer signals changes in the [active] variable, it should not be triggered for events that don't flip the [active] state.
+ */
+ val activeChange: Observable
+}
+
+/**
+ * Simple interface for generic start/stop service lifecycle and the [active] flag indicating runtime ready state.
+ */
+interface ServiceLifecycleSupport : ServiceStateSupport, AutoCloseable {
+ /**
+ * Manual call to allow the service to start the process towards becoming active.
+ * Note wiring up service dependencies should happen in the constructor phase, unless this is to avoid a circular reference.
+ * Also, resources allocated as a result of start should be cleaned up as much as possible by stop.
+ * The [start] method should allow multiple reuse, assuming a [stop] call was made to clear the state.
+ */
+ fun start()
+
+ /**
+ * Release the resources created by [start] and drops the [active] state to false.
+ */
+ fun stop()
+
+ override fun close() = stop()
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/artemis/BridgeArtemisConnectionServiceImpl.kt b/bridge/src/main/kotlin/net/corda/bridge/services/artemis/BridgeArtemisConnectionServiceImpl.kt
new file mode 100644
index 0000000000..97fde6f656
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/artemis/BridgeArtemisConnectionServiceImpl.kt
@@ -0,0 +1,171 @@
+package net.corda.bridge.services.artemis
+
+import net.corda.bridge.services.api.BridgeArtemisConnectionService
+import net.corda.bridge.services.api.BridgeAuditService
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.api.ServiceStateSupport
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.internal.ThreadBox
+import net.corda.core.serialization.internal.nodeSerializationEnv
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.ArtemisTcpTransport
+import net.corda.nodeapi.ConnectionDirection
+import net.corda.nodeapi.internal.ArtemisMessagingClient
+import net.corda.nodeapi.internal.ArtemisMessagingComponent
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import org.apache.activemq.artemis.api.core.client.ActiveMQClient
+import org.apache.activemq.artemis.api.core.client.FailoverEventType
+import org.apache.activemq.artemis.api.core.client.ServerLocator
+import rx.Subscription
+import java.util.concurrent.CountDownLatch
+
+class BridgeArtemisConnectionServiceImpl(val conf: BridgeConfiguration,
+ val maxMessageSize: Int,
+ val auditService: BridgeAuditService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeArtemisConnectionService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private class InnerState {
+ var running = false
+ var locator: ServerLocator? = null
+ var started: ArtemisMessagingClient.Started? = null
+ var connectThread: Thread? = null
+ }
+
+ private val state = ThreadBox(InnerState())
+ private val sslConfiguration: SSLConfiguration
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService))
+ sslConfiguration = conf.outboundConfig?.customSSLConfiguration ?: conf
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ if (it) {
+ startArtemisConnection()
+ } else {
+ stopArtemisConnection()
+ }
+ }
+ }
+
+ private fun startArtemisConnection() {
+ state.locked {
+ check(!running) { "start can't be called twice" }
+ running = true
+ log.info("Connecting to message broker: ${conf.outboundConfig!!.artemisBrokerAddress}")
+ // TODO Add broker CN to config for host verification in case the embedded broker isn't used
+ val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), conf.outboundConfig!!.artemisBrokerAddress, sslConfiguration)
+ locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
+ // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
+ // would be the default and the two lines below can be deleted.
+ connectionTTL = -1
+ clientFailureCheckPeriod = -1
+ minLargeMessageSize = maxMessageSize
+ isUseGlobalPools = nodeSerializationEnv != null
+ }
+ connectThread = Thread({ artemisReconnectionLoop() }, "Artemis Connector Thread").apply {
+ isDaemon = true
+ }
+ connectThread!!.start()
+ }
+ }
+
+ override fun stop() {
+ stopArtemisConnection()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+
+ private fun stopArtemisConnection() {
+ stateHelper.active = false
+ val connectThread = state.locked {
+ if (running) {
+ log.info("Shutdown artemis")
+ running = false
+ started?.apply {
+ producer.close()
+ session.close()
+ sessionFactory.close()
+ }
+ started = null
+ locator?.close()
+ locator = null
+ val thread = connectThread
+ connectThread = null
+ thread
+ } else null
+ }
+ connectThread?.interrupt()
+ connectThread?.join(conf.politeShutdownPeriod.toLong())
+ }
+
+ override val started: ArtemisMessagingClient.Started?
+ get() = state.locked { started }
+
+ private fun artemisReconnectionLoop() {
+ while (state.locked { running }) {
+ val locator = state.locked { locator }
+ if (locator == null) {
+ break
+ }
+ try {
+ log.info("Try create session factory")
+ val newSessionFactory = locator.createSessionFactory()
+ log.info("Got session factory")
+ val latch = CountDownLatch(1)
+ newSessionFactory.connection.addCloseListener {
+ log.info("Connection close event")
+ latch.countDown()
+ }
+ newSessionFactory.addFailoverListener { evt: FailoverEventType ->
+ log.info("Session failover Event $evt")
+ if (evt == FailoverEventType.FAILOVER_FAILED) {
+ latch.countDown()
+ }
+ }
+ val newSession = newSessionFactory.createSession(ArtemisMessagingComponent.NODE_USER,
+ ArtemisMessagingComponent.NODE_USER,
+ false,
+ true,
+ true,
+ locator.isPreAcknowledge,
+ ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
+ newSession.start()
+ log.info("Session created")
+ val newProducer = newSession.createProducer()
+ state.locked {
+ started = ArtemisMessagingClient.Started(locator, newSessionFactory, newSession, newProducer)
+ }
+ stateHelper.active = true
+ latch.await()
+ stateHelper.active = false
+ state.locked {
+ started?.apply {
+ producer.close()
+ session.close()
+ sessionFactory.close()
+ }
+ started = null
+ }
+ log.info("Session closed")
+ } catch (ex: Exception) {
+ log.trace("Caught exception", ex)
+ }
+
+ try {
+ // Sleep for a short while before attempting reconnect
+ Thread.sleep(conf.artemisReconnectionInterval.toLong())
+ } catch (ex: InterruptedException) {
+ // ignore
+ }
+ }
+ log.info("Ended Artemis Connector Thread")
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/audit/LoggingBridgeAuditService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/audit/LoggingBridgeAuditService.kt
new file mode 100644
index 0000000000..829d4605f0
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/audit/LoggingBridgeAuditService.kt
@@ -0,0 +1,45 @@
+package net.corda.bridge.services.audit
+
+import net.corda.bridge.services.api.BridgeAuditService
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.api.ServiceStateSupport
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+import net.corda.core.utilities.trace
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import java.net.InetSocketAddress
+
+class LoggingBridgeAuditService(val conf: BridgeConfiguration,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeAuditService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ override fun start() {
+ stateHelper.active = true
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ }
+
+ override fun successfulConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress, certificateSubject: String, msg: String) {
+ log.info(msg)
+ }
+
+ override fun failedConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress?, certificateSubject: String?, msg: String) {
+ log.warn(msg)
+ }
+
+ override fun packetDropEvent(packet: ReceivedMessage?, msg: String) {
+ log.info(msg)
+ }
+
+ override fun packetAcceptedEvent(packet: ReceivedMessage) {
+ log.trace { "Packet received from ${packet.sourceLegalName} uuid: ${packet.applicationProperties["_AMQ_DUPL_ID"]}" }
+ }
+
+ override fun statusChangeEvent(msg: String) {
+ log.info(msg)
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigHelper.kt b/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigHelper.kt
new file mode 100644
index 0000000000..080d6423ee
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigHelper.kt
@@ -0,0 +1,48 @@
+package net.corda.bridge.services.config
+
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigFactory
+import com.typesafe.config.ConfigFactory.systemEnvironment
+import com.typesafe.config.ConfigFactory.systemProperties
+import com.typesafe.config.ConfigParseOptions
+import com.typesafe.config.ConfigRenderOptions
+import net.corda.core.internal.div
+import net.corda.nodeapi.internal.config.toProperties
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+
+
+fun configOf(vararg pairs: Pair): Config = ConfigFactory.parseMap(mapOf(*pairs))
+operator fun Config.plus(overrides: Map): Config = ConfigFactory.parseMap(overrides).withFallback(this)
+
+object BridgeConfigHelper {
+ const val BRIDGE_PROPERTY_PREFIX = "bridge."
+
+ private val log = LoggerFactory.getLogger(javaClass)
+ fun loadConfig(baseDirectory: Path,
+ configFile: Path = baseDirectory / "bridge.conf",
+ allowMissingConfig: Boolean = false,
+ configOverrides: Config = ConfigFactory.empty()): Config {
+ val parseOptions = ConfigParseOptions.defaults()
+ val defaultConfig = ConfigFactory.parseResources("bridgedefault.conf", parseOptions.setAllowMissing(false))
+ val appConfig = ConfigFactory.parseFile(configFile.toFile(), parseOptions.setAllowMissing(allowMissingConfig))
+ val systemOverrides = systemProperties().bridgeEntriesOnly()
+ val environmentOverrides = systemEnvironment().bridgeEntriesOnly()
+
+ val finalConfig = configOverrides
+ // Add substitution values here
+ .withFallback(systemOverrides) //for database integration tests
+ .withFallback(environmentOverrides) //for database integration tests
+ .withFallback(configOf("baseDirectory" to baseDirectory.toString()))
+ .withFallback(appConfig)
+ .withFallback(defaultConfig)
+ .resolve()
+ log.info("Config:\n${finalConfig.root().render(ConfigRenderOptions.defaults())}")
+ return finalConfig
+ }
+
+ private fun Config.bridgeEntriesOnly(): Config {
+ return ConfigFactory.parseMap(toProperties().filterKeys { (it as String).startsWith(BRIDGE_PROPERTY_PREFIX) }.mapKeys { (it.key as String).removePrefix(BRIDGE_PROPERTY_PREFIX) })
+ }
+
+}
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigurationImpl.kt b/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigurationImpl.kt
new file mode 100644
index 0000000000..4893263907
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/config/BridgeConfigurationImpl.kt
@@ -0,0 +1,61 @@
+package net.corda.bridge.services.config
+
+import com.typesafe.config.Config
+import net.corda.bridge.services.api.*
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.ArtemisMessagingComponent
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import net.corda.nodeapi.internal.config.parseAs
+import java.nio.file.Path
+
+
+fun Config.parseAsBridgeConfiguration(): BridgeConfiguration = parseAs()
+
+data class CustomSSLConfiguration(override val keyStorePassword: String,
+ override val trustStorePassword: String,
+ override val certificatesDirectory: Path) : SSLConfiguration
+
+data class BridgeOutboundConfigurationImpl(override val artemisBrokerAddress: NetworkHostAndPort,
+ override val customSSLConfiguration: CustomSSLConfiguration?) : BridgeOutboundConfiguration
+
+data class BridgeInboundConfigurationImpl(override val listeningAddress: NetworkHostAndPort,
+ override val customSSLConfiguration: CustomSSLConfiguration?) : BridgeInboundConfiguration
+
+data class FloatInnerConfigurationImpl(override val floatAddresses: List,
+ override val expectedCertificateSubject: CordaX500Name,
+ override val customSSLConfiguration: CustomSSLConfiguration?,
+ override val customFloatOuterSSLConfiguration: CustomSSLConfiguration?) : FloatInnerConfiguration
+
+data class FloatOuterConfigurationImpl(override val floatAddress: NetworkHostAndPort,
+ override val expectedCertificateSubject: CordaX500Name,
+ override val customSSLConfiguration: CustomSSLConfiguration?) : FloatOuterConfiguration
+
+data class BridgeConfigurationImpl(
+ override val baseDirectory: Path,
+ override val keyStorePassword: String,
+ override val trustStorePassword: String,
+ override val bridgeMode: BridgeMode,
+ override val networkParametersPath: Path,
+ override val outboundConfig: BridgeOutboundConfigurationImpl?,
+ override val inboundConfig: BridgeInboundConfigurationImpl?,
+ override val floatInnerConfig: FloatInnerConfigurationImpl?,
+ override val floatOuterConfig: FloatOuterConfigurationImpl?,
+ override val haConfig: String?,
+ override val enableAMQPPacketTrace: Boolean,
+ override val artemisReconnectionInterval: Int = 5000,
+ override val politeShutdownPeriod: Int = 1000,
+ override val whitelistedHeaders: List = ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.toList()
+) : BridgeConfiguration {
+ init {
+ if (bridgeMode == BridgeMode.SenderReceiver) {
+ require(inboundConfig != null && outboundConfig != null) { "Missing required configuration" }
+ } else if (bridgeMode == BridgeMode.FloatInner) {
+ require(floatInnerConfig != null && outboundConfig != null) { "Missing required configuration" }
+ } else if (bridgeMode == BridgeMode.FloatOuter) {
+ require(inboundConfig != null && floatOuterConfig != null) { "Missing required configuration" }
+ }
+ }
+}
+
+
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/filter/SimpleMessageFilterService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/filter/SimpleMessageFilterService.kt
new file mode 100644
index 0000000000..ed55c1f179
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/filter/SimpleMessageFilterService.kt
@@ -0,0 +1,99 @@
+package net.corda.bridge.services.filter
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.ArtemisMessagingComponent
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import org.apache.activemq.artemis.api.core.SimpleString
+import org.apache.activemq.artemis.api.core.client.ActiveMQClient
+import org.apache.activemq.artemis.api.core.client.ClientProducer
+import org.apache.activemq.artemis.api.core.client.ClientSession
+import rx.Subscription
+
+class SimpleMessageFilterService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ val artemisConnectionService: BridgeArtemisConnectionService,
+ val bridgeSenderService: BridgeSenderService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : IncomingMessageFilterService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private val whiteListedAMQPHeaders: Set = conf.whitelistedHeaders.toSet()
+ private var inboundSession: ClientSession? = null
+ private var inboundProducer: ClientProducer? = null
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService, artemisConnectionService, bridgeSenderService))
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ if (it) {
+ inboundSession = artemisConnectionService.started!!.sessionFactory.createSession(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
+ inboundProducer = inboundSession!!.createProducer()
+ } else {
+ inboundProducer?.close()
+ inboundProducer = null
+ inboundSession?.close()
+ inboundSession = null
+ }
+ stateHelper.active = it
+ }
+ }
+
+ override fun stop() {
+ inboundProducer?.close()
+ inboundProducer = null
+ inboundSession?.close()
+ inboundSession = null
+ stateHelper.active = false
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+
+ private fun validateMessage(inboundMessage: ReceivedMessage) {
+ if (!active) {
+ throw IllegalStateException("Unable to forward message as Service Dependencies down")
+ }
+ val sourceLegalName = try {
+ CordaX500Name.parse(inboundMessage.sourceLegalName)
+ } catch (ex: IllegalArgumentException) {
+ throw SecurityException("Invalid Legal Name ${inboundMessage.sourceLegalName}")
+ }
+ require(inboundMessage.payload.size > 0) { "No valid payload" }
+ val validInboxTopic = bridgeSenderService.validateReceiveTopic(inboundMessage.topic, sourceLegalName)
+ require(validInboxTopic) { "Topic not a legitimate Inbox for a node on this Artemis Broker ${inboundMessage.topic}" }
+ require(inboundMessage.applicationProperties.keys.all { it!!.toString() in whiteListedAMQPHeaders }) { "Disallowed header present in ${inboundMessage.applicationProperties.keys.map { it.toString() }}" }
+ }
+
+ override fun sendMessageToLocalBroker(inboundMessage: ReceivedMessage) {
+ try {
+ validateMessage(inboundMessage)
+ val session = inboundSession
+ val producer = inboundProducer
+ if (session == null || producer == null) {
+ throw IllegalStateException("No artemis connection to forward message over")
+ }
+ val artemisMessage = session.createMessage(true)
+ for (key in whiteListedAMQPHeaders) {
+ if (inboundMessage.applicationProperties.containsKey(key)) {
+ artemisMessage.putObjectProperty(key, inboundMessage.applicationProperties[key])
+ }
+ }
+ artemisMessage.putStringProperty(P2PMessagingHeaders.bridgedCertificateSubject, SimpleString(inboundMessage.sourceLegalName))
+ artemisMessage.writeBodyBufferBytes(inboundMessage.payload)
+ producer.send(SimpleString(inboundMessage.topic), artemisMessage, { _ -> inboundMessage.complete(true) })
+ auditService.packetAcceptedEvent(inboundMessage)
+ } catch (ex: Exception) {
+ auditService.packetDropEvent(inboundMessage, "Packet Failed validation checks: " + ex.message)
+ inboundMessage.complete(false)
+ }
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/ha/SingleInstanceMasterService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/ha/SingleInstanceMasterService.kt
new file mode 100644
index 0000000000..fc9429c9fa
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/ha/SingleInstanceMasterService.kt
@@ -0,0 +1,27 @@
+package net.corda.bridge.services.ha
+
+import net.corda.bridge.services.api.BridgeAuditService
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.api.BridgeMasterService
+import net.corda.bridge.services.api.ServiceStateSupport
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+
+class SingleInstanceMasterService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeMasterService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ override fun start() {
+ auditService.statusChangeEvent("Single instance master going active immediately.")
+ stateHelper.active = true
+ }
+
+ override fun stop() {
+ auditService.statusChangeEvent("Single instance master stopping")
+ stateHelper.active = false
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/receiver/BridgeAMQPListenerServiceImpl.kt b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/BridgeAMQPListenerServiceImpl.kt
new file mode 100644
index 0000000000..4aeea5bd4a
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/BridgeAMQPListenerServiceImpl.kt
@@ -0,0 +1,128 @@
+package net.corda.bridge.services.receiver
+
+import net.corda.bridge.services.api.BridgeAMQPListenerService
+import net.corda.bridge.services.api.BridgeAuditService
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.api.ServiceStateSupport
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
+import net.corda.nodeapi.internal.crypto.KEYSTORE_TYPE
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
+import net.corda.nodeapi.internal.protonwrapper.netty.ConnectionChange
+import org.slf4j.LoggerFactory
+import rx.Observable
+import rx.Subscription
+import rx.subjects.PublishSubject
+import java.io.ByteArrayInputStream
+import java.security.KeyStore
+import java.util.*
+
+class BridgeAMQPListenerServiceImpl(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeAMQPListenerService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ val consoleLogger = LoggerFactory.getLogger("BasicInfo")
+ }
+
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private var amqpServer: AMQPServer? = null
+ private var keyStorePrivateKeyPassword: CharArray? = null
+ private var onConnectSubscription: Subscription? = null
+ private var onConnectAuditSubscription: Subscription? = null
+ private var onReceiveSubscription: Subscription? = null
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService))
+ }
+
+ override fun provisionKeysAndActivate(keyStoreBytes: ByteArray,
+ keyStorePassword: CharArray,
+ keyStorePrivateKeyPassword: CharArray,
+ trustStoreBytes: ByteArray,
+ trustStorePassword: CharArray) {
+ require(active) { "AuditService must be active" }
+ require(keyStorePassword !== keyStorePrivateKeyPassword) { "keyStorePassword and keyStorePrivateKeyPassword must reference distinct arrays!" }
+ val keyStore = loadKeyStoreAndWipeKeys(keyStoreBytes, keyStorePassword)
+ val trustStore = loadKeyStoreAndWipeKeys(trustStoreBytes, trustStorePassword)
+ val bindAddress = conf.inboundConfig!!.listeningAddress
+ val server = AMQPServer(bindAddress.host, bindAddress.port, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, conf.enableAMQPPacketTrace)
+ onConnectSubscription = server.onConnection.subscribe(_onConnection)
+ onConnectAuditSubscription = server.onConnection.subscribe {
+ if (it.connected) {
+ auditService.successfulConnectionEvent(true, it.remoteAddress, it.remoteCert?.subjectDN?.name
+ ?: "", "Successful AMQP inbound connection")
+ } else {
+ auditService.failedConnectionEvent(true, it.remoteAddress, it.remoteCert?.subjectDN?.name
+ ?: "", "Failed AMQP inbound connection")
+ }
+ }
+ onReceiveSubscription = server.onReceive.subscribe(_onReceive)
+ amqpServer = server
+ server.start()
+ val msg = "Now listening for incoming connections on $bindAddress"
+ auditService.statusChangeEvent(msg)
+ consoleLogger.info(msg)
+ }
+
+ private fun loadKeyStoreAndWipeKeys(keyStoreBytes: ByteArray, keyStorePassword: CharArray): KeyStore {
+ val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
+ ByteArrayInputStream(keyStoreBytes).use {
+ keyStore.load(it, keyStorePassword)
+ }
+ // We overwrite the keys we don't need anymore
+ Arrays.fill(keyStoreBytes, 0xAA.toByte())
+ Arrays.fill(keyStorePassword, 0xAA55.toChar())
+ return keyStore
+ }
+
+ override fun wipeKeysAndDeactivate() {
+ onReceiveSubscription?.unsubscribe()
+ onReceiveSubscription = null
+ onConnectSubscription?.unsubscribe()
+ onConnectSubscription = null
+ onConnectAuditSubscription?.unsubscribe()
+ onConnectAuditSubscription = null
+ if (running) {
+ val msg = "AMQP Listener shutting down"
+ auditService.statusChangeEvent(msg)
+ consoleLogger.info(msg)
+ }
+ amqpServer?.close()
+ amqpServer = null
+ if (keyStorePrivateKeyPassword != null) {
+ // Wipe the old password
+ Arrays.fill(keyStorePrivateKeyPassword, 0xAA55.toChar())
+ keyStorePrivateKeyPassword = null
+ }
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ stateHelper.active = it
+ }
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ wipeKeysAndDeactivate()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+
+ override val running: Boolean
+ get() = amqpServer?.listening ?: false
+
+ private val _onReceive = PublishSubject.create().toSerialized()
+ override val onReceive: Observable
+ get() = _onReceive
+
+ private val _onConnection = PublishSubject.create().toSerialized()
+ override val onConnection: Observable
+ get() = _onConnection
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/receiver/FloatControlListenerService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/FloatControlListenerService.kt
new file mode 100644
index 0000000000..3fa4901857
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/FloatControlListenerService.kt
@@ -0,0 +1,229 @@
+package net.corda.bridge.services.receiver
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.receiver.FloatControlTopics.FLOAT_DATA_TOPIC
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.serialization.SerializationDefaults
+import net.corda.core.serialization.deserialize
+import net.corda.core.serialization.serialize
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
+import net.corda.nodeapi.internal.protonwrapper.netty.ConnectionChange
+import rx.Subscription
+import java.security.KeyStore
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+
+class FloatControlListenerService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ val amqpListener: BridgeAMQPListenerService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : FloatControlService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val lock = ReentrantLock()
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private var incomingMessageSubscriber: Subscription? = null
+ private var connectSubscriber: Subscription? = null
+ private var receiveSubscriber: Subscription? = null
+ private var amqpControlServer: AMQPServer? = null
+ private val sslConfiguration: SSLConfiguration
+ private val keyStore: KeyStore
+ private val keyStorePrivateKeyPassword: String
+ private val trustStore: KeyStore
+ private val floatControlAddress = conf.floatOuterConfig!!.floatAddress
+ private val floatClientName = conf.floatOuterConfig!!.expectedCertificateSubject
+ private var activeConnectionInfo: ConnectionChange? = null
+ private var forwardAddress: NetworkHostAndPort? = null
+ private var forwardLegalName: String? = null
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService, amqpListener))
+ sslConfiguration = conf.floatOuterConfig?.customSSLConfiguration ?: conf
+ keyStore = sslConfiguration.loadSslKeyStore().internal
+ keyStorePrivateKeyPassword = sslConfiguration.keyStorePassword
+ trustStore = sslConfiguration.loadTrustStore().internal
+ }
+
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ if (it) {
+ startControlListener()
+ } else {
+ stopControlListener()
+ }
+ stateHelper.active = it
+ }
+ incomingMessageSubscriber = amqpListener.onReceive.subscribe {
+ forwardReceivedMessage(it)
+ }
+ }
+
+ private fun startControlListener() {
+ lock.withLock {
+ val controlServer = AMQPServer(floatControlAddress.host, floatControlAddress.port, null, null, keyStore, keyStorePrivateKeyPassword, trustStore, conf.enableAMQPPacketTrace)
+ connectSubscriber = controlServer.onConnection.subscribe { onConnectToControl(it) }
+ receiveSubscriber = controlServer.onReceive.subscribe { onControlMessage(it) }
+ amqpControlServer = controlServer
+ controlServer.start()
+ }
+ }
+
+ override fun stop() {
+ lock.withLock {
+ stateHelper.active = false
+ stopControlListener()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+ }
+
+ private fun stopControlListener() {
+ lock.withLock {
+ if (amqpListener.running) {
+ amqpListener.wipeKeysAndDeactivate()
+ }
+ connectSubscriber?.unsubscribe()
+ connectSubscriber = null
+ amqpControlServer?.stop()
+ receiveSubscriber?.unsubscribe()
+ receiveSubscriber = null
+ amqpControlServer = null
+ activeConnectionInfo = null
+ forwardAddress = null
+ forwardLegalName = null
+ incomingMessageSubscriber?.unsubscribe()
+ incomingMessageSubscriber = null
+ }
+ }
+
+ private fun onConnectToControl(connectionChange: ConnectionChange) {
+ auditService.statusChangeEvent("Connection change on float control port $connectionChange")
+ lock.withLock {
+ val currentConnection = activeConnectionInfo
+ if (currentConnection != null) {
+ // If there is a new valid TLS connection kill old connection.
+ // Else if this event signals loss of current connection wipe the keys
+ if (connectionChange.connected || (currentConnection.remoteAddress == connectionChange.remoteAddress)) {
+ if (amqpListener.running) {
+ amqpListener.wipeKeysAndDeactivate()
+ }
+ amqpControlServer?.dropConnection(currentConnection.remoteAddress)
+ activeConnectionInfo = null
+ forwardAddress = null
+ forwardLegalName = null
+ }
+ }
+ if (connectionChange.connected) {
+ if (connectionChange.remoteCert != null) {
+ val certificateSubject = CordaX500Name.parse(connectionChange.remoteCert!!.subjectDN.toString())
+ if (certificateSubject == floatClientName) {
+ activeConnectionInfo = connectionChange
+ } else {
+ amqpControlServer?.dropConnection(connectionChange.remoteAddress)
+ }
+ } else {
+ amqpControlServer?.dropConnection(connectionChange.remoteAddress)
+ }
+ }
+ }
+ }
+
+ private fun onControlMessage(receivedMessage: ReceivedMessage) {
+ if (!receivedMessage.checkTunnelControlTopic()) {
+ auditService.packetDropEvent(receivedMessage, "Invalid control topic packet received on topic ${receivedMessage.topic}!!")
+ receivedMessage.complete(false)
+ return
+ }
+ val controlMessage = try {
+ if (CordaX500Name.parse(receivedMessage.sourceLegalName) != floatClientName) {
+ auditService.packetDropEvent(receivedMessage, "Invalid control source legal name!!")
+ receivedMessage.complete(false)
+ return
+ }
+ receivedMessage.payload.deserialize()
+ } catch (ex: Exception) {
+ receivedMessage.complete(false)
+ return
+ }
+ lock.withLock {
+ when (controlMessage) {
+ is ActivateFloat -> {
+ log.info("Received Tunnel Activate message")
+ amqpListener.provisionKeysAndActivate(controlMessage.keyStoreBytes,
+ controlMessage.keyStorePassword,
+ controlMessage.keyStorePrivateKeyPassword,
+ controlMessage.trustStoreBytes,
+ controlMessage.trustStorePassword)
+ forwardAddress = receivedMessage.destinationLink
+ forwardLegalName = receivedMessage.destinationLegalName
+ }
+ is DeactivateFloat -> {
+ log.info("Received Tunnel Deactivate message")
+ if (amqpListener.running) {
+ amqpListener.wipeKeysAndDeactivate()
+ }
+ forwardAddress = null
+ forwardLegalName = null
+
+ }
+ }
+ }
+ receivedMessage.complete(true)
+ }
+
+ private fun forwardReceivedMessage(message: ReceivedMessage) {
+ val amqpControl = lock.withLock {
+ if (amqpControlServer == null ||
+ activeConnectionInfo == null ||
+ forwardLegalName == null ||
+ forwardAddress == null ||
+ !stateHelper.active) {
+ null
+ } else {
+ amqpControlServer
+ }
+ }
+ if (amqpControl == null) {
+ message.complete(false)
+ return
+ }
+ if (!message.topic.startsWith(P2P_PREFIX)) {
+ auditService.packetDropEvent(message, "Message topic is not a valid peer namespace ${message.topic}")
+ message.complete(false)
+ return
+ }
+ val appProperties = message.applicationProperties.map { Pair(it.key!!.toString(), it.value) }.toList()
+ try {
+ val wrappedMessage = FloatDataPacket(message.topic,
+ appProperties,
+ message.payload,
+ CordaX500Name.parse(message.sourceLegalName),
+ message.sourceLink,
+ CordaX500Name.parse(message.destinationLegalName),
+ message.destinationLink)
+ val amqpForwardMessage = amqpControl.createMessage(wrappedMessage.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes,
+ FLOAT_DATA_TOPIC,
+ forwardLegalName!!,
+ forwardAddress!!,
+ emptyMap())
+ amqpForwardMessage.onComplete.then { message.complete(it.get() == MessageStatus.Acknowledged) }
+ amqpControl.write(amqpForwardMessage)
+ } catch (ex: Exception) {
+ log.error("Failed to forward message", ex)
+ message.complete(false)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/receiver/InProcessBridgeReceiverService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/InProcessBridgeReceiverService.kt
new file mode 100644
index 0000000000..3ec949ad25
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/InProcessBridgeReceiverService.kt
@@ -0,0 +1,64 @@
+package net.corda.bridge.services.receiver
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.internal.readAll
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import rx.Subscription
+
+class InProcessBridgeReceiverService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ haService: BridgeMasterService,
+ val amqpListenerService: BridgeAMQPListenerService,
+ val filterService: IncomingMessageFilterService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeReceiverService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private var receiveSubscriber: Subscription? = null
+ private val sslConfiguration: SSLConfiguration
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService, haService, amqpListenerService, filterService))
+ sslConfiguration = conf.inboundConfig?.customSSLConfiguration ?: conf
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ if (it) {
+ val keyStoreBytes = sslConfiguration.sslKeystore.readAll()
+ val trustStoreBytes = sslConfiguration.trustStoreFile.readAll()
+ amqpListenerService.provisionKeysAndActivate(keyStoreBytes,
+ sslConfiguration.keyStorePassword.toCharArray(),
+ sslConfiguration.keyStorePassword.toCharArray(),
+ trustStoreBytes,
+ sslConfiguration.trustStorePassword.toCharArray())
+ }
+ stateHelper.active = it
+ }
+ receiveSubscriber = amqpListenerService.onReceive.subscribe {
+ processMessage(it)
+ }
+ }
+
+ private fun processMessage(receivedMessage: ReceivedMessage) {
+ filterService.sendMessageToLocalBroker(receivedMessage)
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ if (amqpListenerService.running) {
+ amqpListenerService.wipeKeysAndDeactivate()
+ }
+ receiveSubscriber?.unsubscribe()
+ receiveSubscriber = null
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelControlMessages.kt b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelControlMessages.kt
new file mode 100644
index 0000000000..62b8e64bcb
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelControlMessages.kt
@@ -0,0 +1,37 @@
+package net.corda.bridge.services.receiver
+
+import net.corda.bridge.services.receiver.FloatControlTopics.FLOAT_CONTROL_TOPIC
+import net.corda.bridge.services.receiver.FloatControlTopics.FLOAT_DATA_TOPIC
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+
+@CordaSerializable
+sealed class TunnelControlMessage
+
+object FloatControlTopics {
+ const val FLOAT_CONTROL_TOPIC = "float.control"
+ const val FLOAT_DATA_TOPIC = "float.forward"
+}
+
+internal class ActivateFloat(val keyStoreBytes: ByteArray,
+ val keyStorePassword: CharArray,
+ val keyStorePrivateKeyPassword: CharArray,
+ val trustStoreBytes: ByteArray,
+ val trustStorePassword: CharArray) : TunnelControlMessage()
+
+class DeactivateFloat : TunnelControlMessage()
+
+fun ReceivedMessage.checkTunnelControlTopic() = (topic == FLOAT_CONTROL_TOPIC)
+
+@CordaSerializable
+internal class FloatDataPacket(val topic: String,
+ val originalHeaders: List>,
+ val originalPayload: ByteArray,
+ val sourceLegalName: CordaX500Name,
+ val sourceLink: NetworkHostAndPort,
+ val destinationLegalName: CordaX500Name,
+ val destinationLink: NetworkHostAndPort)
+
+fun ReceivedMessage.checkTunnelDataTopic() = (topic == FLOAT_DATA_TOPIC)
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelingBridgeReceiverService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelingBridgeReceiverService.kt
new file mode 100644
index 0000000000..2dbc82155c
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/receiver/TunnelingBridgeReceiverService.kt
@@ -0,0 +1,193 @@
+package net.corda.bridge.services.receiver
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.receiver.FloatControlTopics.FLOAT_CONTROL_TOPIC
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.crypto.newSecureRandom
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.internal.readAll
+import net.corda.core.serialization.SerializationDefaults
+import net.corda.core.serialization.deserialize
+import net.corda.core.serialization.serialize
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
+import net.corda.nodeapi.internal.protonwrapper.netty.ConnectionChange
+import rx.Subscription
+import java.io.ByteArrayOutputStream
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class TunnelingBridgeReceiverService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ haService: BridgeMasterService,
+ val filterService: IncomingMessageFilterService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeReceiverService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private var connectSubscriber: Subscription? = null
+ private var receiveSubscriber: Subscription? = null
+ private var amqpControlClient: AMQPClient? = null
+ private val controlLinkSSLConfiguration: SSLConfiguration
+ private val floatListenerSSLConfiguration: SSLConfiguration
+ private val controlLinkKeyStore: KeyStore
+ private val controLinkKeyStorePrivateKeyPassword: String
+ private val controlLinkTrustStore: KeyStore
+ private val expectedCertificateSubject: CordaX500Name
+ private val secureRandom: SecureRandom = newSecureRandom()
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService, haService, filterService))
+ controlLinkSSLConfiguration = conf.floatInnerConfig?.customSSLConfiguration ?: conf
+ floatListenerSSLConfiguration = conf.floatInnerConfig?.customFloatOuterSSLConfiguration ?: conf
+ controlLinkKeyStore = controlLinkSSLConfiguration.loadSslKeyStore().internal
+ controLinkKeyStorePrivateKeyPassword = controlLinkSSLConfiguration.keyStorePassword
+ controlLinkTrustStore = controlLinkSSLConfiguration.loadTrustStore().internal
+ expectedCertificateSubject = conf.floatInnerConfig!!.expectedCertificateSubject
+ }
+
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ if (it) {
+ val floatAddresses = conf.floatInnerConfig!!.floatAddresses
+ val controlClient = AMQPClient(floatAddresses, setOf(expectedCertificateSubject), null, null, controlLinkKeyStore, controLinkKeyStorePrivateKeyPassword, controlLinkTrustStore, conf.enableAMQPPacketTrace)
+ connectSubscriber = controlClient.onConnection.subscribe { onConnectToControl(it) }
+ receiveSubscriber = controlClient.onReceive.subscribe { onFloatMessage(it) }
+ amqpControlClient = controlClient
+ controlClient.start()
+ } else {
+ stateHelper.active = false
+ closeAMQPClient()
+ }
+ }
+ }
+
+ private fun closeAMQPClient() {
+ connectSubscriber?.unsubscribe()
+ connectSubscriber = null
+ receiveSubscriber?.unsubscribe()
+ receiveSubscriber = null
+ amqpControlClient?.apply {
+ val deactivateMessage = DeactivateFloat()
+ val amqpDeactivateMessage = amqpControlClient!!.createMessage(deactivateMessage.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes,
+ FLOAT_CONTROL_TOPIC,
+ expectedCertificateSubject.toString(),
+ emptyMap())
+ try {
+ amqpControlClient!!.write(amqpDeactivateMessage)
+ } catch (ex: IllegalStateException) {
+ // ignore if channel is already closed
+ }
+ try {
+ // Await acknowledgement of the deactivate message, but don't block our shutdown forever.
+ amqpDeactivateMessage.onComplete.get(conf.politeShutdownPeriod.toLong(), TimeUnit.MILLISECONDS)
+ } catch (ex: TimeoutException) {
+ // Ignore
+ }
+ stop()
+ }
+ amqpControlClient = null
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ closeAMQPClient()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+
+ private fun onConnectToControl(connectionChange: ConnectionChange) {
+ auditService.statusChangeEvent("Connection change on float control port $connectionChange")
+ if (connectionChange.connected) {
+ val (freshKeyStorePassword, freshKeyStoreKeyPassword, recodedKeyStore) = recodeKeyStore(floatListenerSSLConfiguration)
+ val trustStoreBytes = floatListenerSSLConfiguration.trustStoreFile.readAll()
+ val activateMessage = ActivateFloat(recodedKeyStore,
+ freshKeyStorePassword,
+ freshKeyStoreKeyPassword,
+ trustStoreBytes,
+ floatListenerSSLConfiguration.trustStorePassword.toCharArray())
+ val amqpActivateMessage = amqpControlClient!!.createMessage(activateMessage.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes,
+ FLOAT_CONTROL_TOPIC,
+ expectedCertificateSubject.toString(),
+ emptyMap())
+ try {
+ amqpControlClient!!.write(amqpActivateMessage)
+ } catch (ex: IllegalStateException) {
+ stateHelper.active = false // lost the channel
+ return
+ }
+ amqpActivateMessage.onComplete.then {
+ stateHelper.active = (it.get() == MessageStatus.Acknowledged)
+ //TODO Retry?
+ }
+ } else {
+ stateHelper.active = false
+ }
+ }
+
+ // Recode KeyStore to use a fresh random password for entries and overall
+ private fun recodeKeyStore(sslConfiguration: SSLConfiguration): Triple {
+ val keyStoreOriginal = sslConfiguration.loadSslKeyStore().internal
+ val originalKeyStorePassword = sslConfiguration.keyStorePassword.toCharArray()
+ val freshKeyStorePassword = CharArray(20) { secureRandom.nextInt(0xD800).toChar() } // Stick to single character Unicode range
+ val freshPrivateKeyPassword = CharArray(20) { secureRandom.nextInt(0xD800).toChar() } // Stick to single character Unicode range
+ for (alias in keyStoreOriginal.aliases()) {
+ if (keyStoreOriginal.isKeyEntry(alias)) {
+ // Recode key entries to new password
+ val privateKey = keyStoreOriginal.getKey(alias, originalKeyStorePassword)
+ val certs = keyStoreOriginal.getCertificateChain(alias)
+ keyStoreOriginal.setKeyEntry(alias, privateKey, freshPrivateKeyPassword, certs)
+ }
+ }
+ // Serialize re-keyed KeyStore to ByteArray
+ val recodedKeyStore = ByteArrayOutputStream().use {
+ keyStoreOriginal.store(it, freshKeyStorePassword)
+ it
+ }.toByteArray()
+
+ return Triple(freshKeyStorePassword, freshPrivateKeyPassword, recodedKeyStore)
+ }
+
+ private fun onFloatMessage(receivedMessage: ReceivedMessage) {
+ if (!receivedMessage.checkTunnelDataTopic()) {
+ auditService.packetDropEvent(receivedMessage, "Invalid float inbound topic received ${receivedMessage.topic}!!")
+ receivedMessage.complete(false)
+ return
+ }
+ val innerMessage = try {
+ receivedMessage.payload.deserialize()
+ } catch (ex: Exception) {
+ auditService.packetDropEvent(receivedMessage, "Unable to decode Float Control message")
+ receivedMessage.complete(false)
+ return
+ }
+ log.info("Received $innerMessage")
+ val onwardMessage = object : ReceivedMessage {
+ override val topic: String = innerMessage.topic
+ override val applicationProperties: Map = innerMessage.originalHeaders.toMap()
+ override val payload: ByteArray = innerMessage.originalPayload
+ override val sourceLegalName: String = innerMessage.sourceLegalName.toString()
+ override val sourceLink: NetworkHostAndPort = receivedMessage.sourceLink
+
+ override fun complete(accepted: Boolean) {
+ receivedMessage.complete(accepted)
+ }
+
+ override val destinationLegalName: String = innerMessage.destinationLegalName.toString()
+ override val destinationLink: NetworkHostAndPort = innerMessage.destinationLink
+ }
+ filterService.sendMessageToLocalBroker(onwardMessage)
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/sender/DirectBridgeSenderService.kt b/bridge/src/main/kotlin/net/corda/bridge/services/sender/DirectBridgeSenderService.kt
new file mode 100644
index 0000000000..3409dfa80d
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/sender/DirectBridgeSenderService.kt
@@ -0,0 +1,68 @@
+package net.corda.bridge.services.sender
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.ArtemisMessagingClient
+import net.corda.nodeapi.internal.ArtemisSessionProvider
+import net.corda.nodeapi.internal.bridging.BridgeControlListener
+import rx.Subscription
+
+class DirectBridgeSenderService(val conf: BridgeConfiguration,
+ val auditService: BridgeAuditService,
+ val haService: BridgeMasterService,
+ val artemisConnectionService: BridgeArtemisConnectionService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeSenderService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ }
+
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+ private var connectionSubscriber: Subscription? = null
+ private var bridgeControlListener: BridgeControlListener = BridgeControlListener(conf, { ForwardingArtemisMessageClient(artemisConnectionService) })
+
+ init {
+ statusFollower = ServiceStateCombiner(listOf(auditService, artemisConnectionService, haService))
+ }
+
+ private class ForwardingArtemisMessageClient(val artemisConnectionService: BridgeArtemisConnectionService) : ArtemisSessionProvider {
+ override fun start(): ArtemisMessagingClient.Started {
+ // We don't want to start and stop artemis from clients as the lifecycle management is provided by the BridgeArtemisConnectionService
+ return artemisConnectionService.started!!
+ }
+
+ override fun stop() {
+ // We don't want to start and stop artemis from clients as the lifecycle management is provided by the BridgeArtemisConnectionService
+ }
+
+ override val started: ArtemisMessagingClient.Started?
+ get() = artemisConnectionService.started
+
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe { ready ->
+ if (ready) {
+ bridgeControlListener.start()
+ stateHelper.active = true
+ } else {
+ stateHelper.active = false
+ bridgeControlListener.stop()
+ }
+ }
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ bridgeControlListener.stop()
+ connectionSubscriber?.unsubscribe()
+ connectionSubscriber = null
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+
+ override fun validateReceiveTopic(topic: String, sourceLegalName: CordaX500Name): Boolean = bridgeControlListener.validateReceiveTopic(topic)
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/BridgeSupervisorServiceImpl.kt b/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/BridgeSupervisorServiceImpl.kt
new file mode 100644
index 0000000000..e70542227b
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/BridgeSupervisorServiceImpl.kt
@@ -0,0 +1,76 @@
+package net.corda.bridge.services.supervisors
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.artemis.BridgeArtemisConnectionServiceImpl
+import net.corda.bridge.services.filter.SimpleMessageFilterService
+import net.corda.bridge.services.ha.SingleInstanceMasterService
+import net.corda.bridge.services.receiver.InProcessBridgeReceiverService
+import net.corda.bridge.services.receiver.TunnelingBridgeReceiverService
+import net.corda.bridge.services.sender.DirectBridgeSenderService
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+import org.slf4j.LoggerFactory
+import rx.Subscription
+
+class BridgeSupervisorServiceImpl(val conf: BridgeConfiguration,
+ maxMessageSize: Int,
+ val auditService: BridgeAuditService,
+ inProcessAMQPListenerService: BridgeAMQPListenerService?,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : BridgeSupervisorService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ val consoleLogger = LoggerFactory.getLogger("BasicInfo")
+ }
+
+ private val haService: BridgeMasterService
+ private val artemisService: BridgeArtemisConnectionServiceImpl
+ private val senderService: BridgeSenderService
+ private val receiverService: BridgeReceiverService
+ private val filterService: IncomingMessageFilterService
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+
+ init {
+ if (conf.haConfig.isNullOrEmpty()) {
+ haService = SingleInstanceMasterService(conf, auditService)
+ } else {
+ TODO()
+ }
+ artemisService = BridgeArtemisConnectionServiceImpl(conf, maxMessageSize, auditService)
+ senderService = DirectBridgeSenderService(conf, auditService, haService, artemisService)
+ filterService = SimpleMessageFilterService(conf, auditService, artemisService, senderService)
+ receiverService = if (conf.bridgeMode == BridgeMode.SenderReceiver) {
+ InProcessBridgeReceiverService(conf, auditService, haService, inProcessAMQPListenerService!!, filterService)
+ } else {
+ require(inProcessAMQPListenerService == null) { "Should not have an in process instance of the AMQPListenerService" }
+ TunnelingBridgeReceiverService(conf, auditService, haService, filterService)
+ }
+ statusFollower = ServiceStateCombiner(listOf(haService, senderService, receiverService, filterService))
+ activeChange.subscribe {
+ consoleLogger.info("BridgeSupervisorService: active = $it")
+ }
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ stateHelper.active = it
+ }
+ artemisService.start()
+ senderService.start()
+ receiverService.start()
+ filterService.start()
+ haService.start()
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ haService.stop()
+ senderService.stop()
+ receiverService.stop()
+ filterService.stop()
+ artemisService.stop()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/FloatSupervisorServiceImpl.kt b/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/FloatSupervisorServiceImpl.kt
new file mode 100644
index 0000000000..e588ecc0ab
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/supervisors/FloatSupervisorServiceImpl.kt
@@ -0,0 +1,54 @@
+package net.corda.bridge.services.supervisors
+
+import net.corda.bridge.services.api.*
+import net.corda.bridge.services.receiver.BridgeAMQPListenerServiceImpl
+import net.corda.bridge.services.receiver.FloatControlListenerService
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+import org.slf4j.LoggerFactory
+import rx.Subscription
+
+class FloatSupervisorServiceImpl(val conf: BridgeConfiguration,
+ val maxMessageSize: Int,
+ val auditService: BridgeAuditService,
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(log)) : FloatSupervisorService, ServiceStateSupport by stateHelper {
+ companion object {
+ val log = contextLogger()
+ val consoleLogger = LoggerFactory.getLogger("BasicInfo")
+ }
+
+ override val amqpListenerService: BridgeAMQPListenerService
+ private val floatControlService: FloatControlService?
+ private val statusFollower: ServiceStateCombiner
+ private var statusSubscriber: Subscription? = null
+
+ init {
+ amqpListenerService = BridgeAMQPListenerServiceImpl(conf, auditService)
+ floatControlService = if (conf.bridgeMode == BridgeMode.FloatOuter) {
+ FloatControlListenerService(conf, auditService, amqpListenerService)
+ } else {
+ null
+ }
+ statusFollower = ServiceStateCombiner(listOf(amqpListenerService, floatControlService).filterNotNull())
+ activeChange.subscribe {
+ consoleLogger.info("FloatSupervisorService: active = $it")
+ }
+ }
+
+ override fun start() {
+ statusSubscriber = statusFollower.activeChange.subscribe {
+ stateHelper.active = it
+ }
+ amqpListenerService.start()
+ floatControlService?.start()
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ floatControlService?.stop()
+ amqpListenerService.stop()
+ statusSubscriber?.unsubscribe()
+ statusSubscriber = null
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/main/kotlin/net/corda/bridge/services/util/ServiceStateHelper.kt b/bridge/src/main/kotlin/net/corda/bridge/services/util/ServiceStateHelper.kt
new file mode 100644
index 0000000000..28f4429910
--- /dev/null
+++ b/bridge/src/main/kotlin/net/corda/bridge/services/util/ServiceStateHelper.kt
@@ -0,0 +1,44 @@
+package net.corda.bridge.services.util
+
+import net.corda.bridge.services.api.ServiceStateSupport
+import org.slf4j.Logger
+import rx.Observable
+import rx.subjects.BehaviorSubject
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Simple implementation of [ServiceStateSupport] service domino logic using RxObservables.
+ */
+class ServiceStateHelper(val log: Logger) : ServiceStateSupport {
+ val lock = ReentrantLock()
+ private var _active: Boolean = false
+ override var active: Boolean
+ get() = lock.withLock { _active }
+ set(value) {
+ lock.withLock {
+ if (value != _active) {
+ _active = value
+ log.info("Status change to $value")
+ _activeChange.onNext(value)
+ }
+ }
+ }
+
+ private val _activeChange: BehaviorSubject = BehaviorSubject.create(false)
+ private val _threadSafeObservable: Observable = _activeChange.serialize().distinctUntilChanged()
+ override val activeChange: Observable
+ get() = _threadSafeObservable
+}
+
+/**
+ * Simple implementation of [ServiceStateSupport] where it only reports [active] true when a set of dependencies are all [active] true.
+ */
+class ServiceStateCombiner(val services: List) : ServiceStateSupport {
+ override val active: Boolean
+ get() = services.all { it.active }
+
+ private val _activeChange = Observable.combineLatest(services.map { it.activeChange }, { x -> x.all { y -> y as Boolean } }).serialize().distinctUntilChanged()
+ override val activeChange: Observable
+ get() = _activeChange
+}
\ No newline at end of file
diff --git a/bridge/src/main/resources/bridgedefault.conf b/bridge/src/main/resources/bridgedefault.conf
new file mode 100644
index 0000000000..508a5b47ed
--- /dev/null
+++ b/bridge/src/main/resources/bridgedefault.conf
@@ -0,0 +1,5 @@
+keyStorePassword = "cordacadevpass"
+trustStorePassword = "trustpass"
+enableAMQPPacketTrace = false
+artemisReconnectionInterval = 5000
+politeShutdownPeriod = 1000
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/BridgeTestHelper.kt b/bridge/src/test/kotlin/net/corda/bridge/BridgeTestHelper.kt
new file mode 100644
index 0000000000..f6e4ee46e2
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/BridgeTestHelper.kt
@@ -0,0 +1,94 @@
+package net.corda.bridge
+
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.core.crypto.Crypto.generateKeyPair
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.internal.createDirectories
+import net.corda.core.internal.exists
+import net.corda.core.node.NetworkParameters
+import net.corda.core.node.NotaryInfo
+import net.corda.core.node.services.AttachmentId
+import net.corda.nodeapi.internal.*
+import net.corda.nodeapi.internal.config.SSLConfiguration
+import net.corda.nodeapi.internal.crypto.*
+import net.corda.nodeapi.internal.network.NetworkParametersCopier
+import net.corda.testing.core.DUMMY_NOTARY_NAME
+import net.corda.testing.core.TestIdentity
+import org.mockito.Mockito
+import org.mockito.Mockito.CALLS_REAL_METHODS
+import org.mockito.Mockito.withSettings
+import java.net.Socket
+import java.nio.file.Files
+import java.nio.file.Path
+import java.security.cert.X509Certificate
+import java.time.Instant
+
+fun createNetworkParams(baseDirectory: Path) {
+ val dummyNotaryParty = TestIdentity(DUMMY_NOTARY_NAME)
+ val notaryInfo = NotaryInfo(dummyNotaryParty.party, false)
+ val copier = NetworkParametersCopier(NetworkParameters(
+ minimumPlatformVersion = 1,
+ notaries = listOf(notaryInfo),
+ modifiedTime = Instant.now(),
+ maxMessageSize = 10485760,
+ maxTransactionSize = 40000,
+ epoch = 1,
+ whitelistedContractImplementations = emptyMap>()
+ ), overwriteFile = true)
+ copier.install(baseDirectory)
+}
+
+
+fun createAndLoadConfigFromResource(baseDirectory: Path, configResource: String): BridgeConfiguration {
+ val workspaceFolder = baseDirectory.normalize().toAbsolutePath()
+ val args = arrayOf("--base-directory", workspaceFolder.toString())
+ val argsParser = ArgsParser()
+ val cmdlineOptions = argsParser.parse(*args)
+ val configFile = cmdlineOptions.configFile
+ configFile.normalize().parent?.createDirectories()
+ ConfigTest::class.java.getResourceAsStream(configResource).use {
+ Files.copy(it, configFile)
+ }
+ val config = cmdlineOptions.loadConfig()
+ return config
+}
+
+fun SSLConfiguration.createBridgeKeyStores(legalName: CordaX500Name,
+ rootCert: X509Certificate = DEV_ROOT_CA.certificate,
+ intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA) {
+
+ certificatesDirectory.createDirectories()
+ if (!trustStoreFile.exists()) {
+ loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/${DEV_CA_TRUST_STORE_FILE}"), DEV_CA_TRUST_STORE_PASS).save(trustStoreFile, trustStorePassword)
+ }
+
+ val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
+
+ val sslKeyStore = loadSslKeyStore(createNew = true)
+ sslKeyStore.update {
+ val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+ val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public)
+ setPrivateKey(
+ X509Utilities.CORDA_CLIENT_TLS,
+ tlsKeyPair.private,
+ listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert))
+ }
+}
+
+
+fun serverListening(host: String, port: Int): Boolean {
+ var s: Socket? = null
+ try {
+ s = Socket(host, port)
+ return true
+ } catch (e: Exception) {
+ return false
+ } finally {
+ try {
+ s?.close()
+ } catch (e: Exception) {
+ }
+ }
+}
+
+inline fun createPartialMock() = Mockito.mock(T::class.java, withSettings().useConstructor().defaultAnswer(CALLS_REAL_METHODS))
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/ConfigTest.kt b/bridge/src/test/kotlin/net/corda/bridge/ConfigTest.kt
new file mode 100644
index 0000000000..1956424064
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/ConfigTest.kt
@@ -0,0 +1,56 @@
+package net.corda.bridge
+
+import net.corda.bridge.services.api.BridgeMode
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.testing.core.SerializationEnvironmentRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class ConfigTest {
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ @Rule
+ @JvmField
+ val serializationEnvironment = SerializationEnvironmentRule()
+
+ @Test
+ fun `Load simple config`() {
+ val configResource = "/net/corda/bridge/singleprocess/bridge.conf"
+ val config = createAndLoadConfigFromResource(tempFolder.root.toPath(), configResource)
+ assertEquals(BridgeMode.SenderReceiver, config.bridgeMode)
+ assertEquals(NetworkHostAndPort("localhost", 11005), config.outboundConfig!!.artemisBrokerAddress)
+ assertEquals(NetworkHostAndPort("0.0.0.0", 10005), config.inboundConfig!!.listeningAddress)
+ assertNull(config.floatInnerConfig)
+ assertNull(config.floatOuterConfig)
+ }
+
+ @Test
+ fun `Load simple bridge config`() {
+ val configResource = "/net/corda/bridge/withfloat/bridge/bridge.conf"
+ val config = createAndLoadConfigFromResource(tempFolder.root.toPath(), configResource)
+ assertEquals(BridgeMode.FloatInner, config.bridgeMode)
+ assertEquals(NetworkHostAndPort("localhost", 11005), config.outboundConfig!!.artemisBrokerAddress)
+ assertNull(config.inboundConfig)
+ assertEquals(listOf(NetworkHostAndPort("localhost", 12005)), config.floatInnerConfig!!.floatAddresses)
+ assertEquals(CordaX500Name.parse("O=Bank A, L=London, C=GB"), config.floatInnerConfig!!.expectedCertificateSubject)
+ assertNull(config.floatOuterConfig)
+ }
+
+ @Test
+ fun `Load simple float config`() {
+ val configResource = "/net/corda/bridge/withfloat/float/bridge.conf"
+ val config = createAndLoadConfigFromResource(tempFolder.root.toPath(), configResource)
+ assertEquals(BridgeMode.FloatOuter, config.bridgeMode)
+ assertNull(config.outboundConfig)
+ assertEquals(NetworkHostAndPort("0.0.0.0", 10005), config.inboundConfig!!.listeningAddress)
+ assertNull(config.floatInnerConfig)
+ assertEquals(NetworkHostAndPort("localhost", 12005), config.floatOuterConfig!!.floatAddress)
+ assertEquals(CordaX500Name.parse("O=Bank A, L=London, C=GB"), config.floatOuterConfig!!.expectedCertificateSubject)
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/ServiceStateTest.kt b/bridge/src/test/kotlin/net/corda/bridge/ServiceStateTest.kt
new file mode 100644
index 0000000000..599c542291
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/ServiceStateTest.kt
@@ -0,0 +1,227 @@
+package net.corda.bridge
+
+import net.corda.bridge.services.api.ServiceStateSupport
+import net.corda.bridge.services.util.ServiceStateCombiner
+import net.corda.bridge.services.util.ServiceStateHelper
+import net.corda.core.utilities.contextLogger
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import rx.Observable
+
+class ServiceStateTest {
+ interface ServiceA : ServiceStateSupport {
+ fun changeStatus(status: Boolean)
+ }
+
+
+ class ServiceAImpl(private val stateSupport: ServiceStateHelper = ServiceStateHelper(log)) : ServiceA, ServiceStateSupport by stateSupport {
+ companion object {
+ val log = contextLogger()
+ }
+
+ override fun changeStatus(status: Boolean) {
+ stateSupport.active = status
+ }
+ }
+
+ interface ServiceB : ServiceStateSupport {
+ fun changeStatus(status: Boolean)
+ }
+
+
+ class ServiceBImpl(private val stateSupport: ServiceStateHelper = ServiceStateHelper(log)) : ServiceB, ServiceStateSupport by stateSupport {
+ companion object {
+ val log = contextLogger()
+ }
+
+ override fun changeStatus(status: Boolean) {
+ stateSupport.active = status
+ }
+ }
+
+ interface ServiceC : ServiceStateSupport {
+ }
+
+
+ class ServiceCImpl(servA: ServiceA, servB: ServiceB) : ServiceC {
+ private val combiner = ServiceStateCombiner(listOf(servA, servB))
+
+ override val active: Boolean
+ get() = combiner.active
+
+ override val activeChange: Observable
+ get() = combiner.activeChange
+ }
+
+ @Test
+ fun `Test state helper`() {
+ val servA = ServiceAImpl()
+ var upA = 0
+ var downA = 0
+ val subsA = servA.activeChange.subscribe {
+ if (it) ++upA else ++downA
+ }
+ assertEquals(0, upA)
+ assertEquals(1, downA)
+
+ servA.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(1, upA)
+ assertEquals(1, downA)
+
+ servA.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(1, upA)
+ assertEquals(1, downA)
+
+ servA.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+
+ servA.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+
+ // Should stop alerting, but keep functioning after unsubscribe
+ subsA.unsubscribe()
+
+ servA.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+
+ servA.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+
+ }
+
+ @Test
+ fun `Test basic domino behaviour of combiner`() {
+ val servA = ServiceAImpl()
+ val servB = ServiceBImpl()
+ val servC = ServiceCImpl(servA, servB)
+ var upA = 0
+ var downA = 0
+ var upB = 0
+ var downB = 0
+ var upC = 0
+ var downC = 0
+ val subsA = servA.activeChange.subscribe {
+ if (it) ++upA else ++downA
+ }
+ val subsB = servB.activeChange.subscribe {
+ if (it) ++upB else ++downB
+ }
+ val subsC = servC.activeChange.subscribe {
+ if (it) ++upC else ++downC
+ }
+ // Get one automatic down event at subscribe
+ assertEquals(false, servA.active)
+ assertEquals(false, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(0, upA)
+ assertEquals(1, downA)
+ assertEquals(0, upB)
+ assertEquals(1, downB)
+ assertEquals(0, upC)
+ assertEquals(1, downC)
+
+ // Rest of sequence should only signal on change and C should come up if A.active && B.active else it is false
+ servA.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(false, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(1, upA)
+ assertEquals(1, downA)
+ assertEquals(0, upB)
+ assertEquals(1, downB)
+ assertEquals(0, upC)
+ assertEquals(1, downC)
+
+ servB.changeStatus(false)
+ assertEquals(true, servA.active)
+ assertEquals(false, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(1, upA)
+ assertEquals(1, downA)
+ assertEquals(0, upB)
+ assertEquals(1, downB)
+ assertEquals(0, upC)
+ assertEquals(1, downC)
+
+ servB.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(true, servB.active)
+ assertEquals(true, servC.active)
+ assertEquals(1, upA)
+ assertEquals(1, downA)
+ assertEquals(1, upB)
+ assertEquals(1, downB)
+ assertEquals(1, upC)
+ assertEquals(1, downC)
+
+ servA.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(true, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+ assertEquals(1, upB)
+ assertEquals(1, downB)
+ assertEquals(1, upC)
+ assertEquals(2, downC)
+
+ servB.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(false, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+ assertEquals(1, upB)
+ assertEquals(2, downB)
+ assertEquals(1, upC)
+ assertEquals(2, downC)
+
+ servB.changeStatus(true)
+ assertEquals(false, servA.active)
+ assertEquals(true, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(1, upA)
+ assertEquals(2, downA)
+ assertEquals(2, upB)
+ assertEquals(2, downB)
+ assertEquals(1, upC)
+ assertEquals(2, downC)
+
+ servA.changeStatus(true)
+ assertEquals(true, servA.active)
+ assertEquals(true, servB.active)
+ assertEquals(true, servC.active)
+ assertEquals(2, upA)
+ assertEquals(2, downA)
+ assertEquals(2, upB)
+ assertEquals(2, downB)
+ assertEquals(2, upC)
+ assertEquals(2, downC)
+
+ subsC.unsubscribe()
+ subsA.unsubscribe()
+ subsB.unsubscribe()
+
+ servA.changeStatus(false)
+ assertEquals(false, servA.active)
+ assertEquals(true, servB.active)
+ assertEquals(false, servC.active)
+ assertEquals(2, upA)
+ assertEquals(2, downA)
+ assertEquals(2, upB)
+ assertEquals(2, downB)
+ assertEquals(2, upC)
+ assertEquals(2, downC)
+
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/services/FilterServiceTest.kt b/bridge/src/test/kotlin/net/corda/bridge/services/FilterServiceTest.kt
new file mode 100644
index 0000000000..e95dd08e5d
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/services/FilterServiceTest.kt
@@ -0,0 +1,215 @@
+package net.corda.bridge.services
+
+import com.nhaarman.mockito_kotlin.*
+import net.corda.bridge.createPartialMock
+import net.corda.bridge.services.api.BridgeArtemisConnectionService
+import net.corda.bridge.services.api.BridgeConfiguration
+import net.corda.bridge.services.api.BridgeSenderService
+import net.corda.bridge.services.filter.SimpleMessageFilterService
+import net.corda.nodeapi.internal.ArtemisMessagingClient
+import net.corda.nodeapi.internal.ArtemisMessagingComponent
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import net.corda.testing.core.DUMMY_BANK_B_NAME
+import net.corda.testing.internal.rigorousMock
+import org.apache.activemq.artemis.api.core.SimpleString
+import org.apache.activemq.artemis.api.core.client.*
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import kotlin.test.assertEquals
+
+class FilterServiceTest {
+ private abstract class TestBridgeArtemisConnectionService : BridgeArtemisConnectionService, TestServiceBase()
+ private abstract class TestBridgeSenderService : BridgeSenderService, TestServiceBase()
+
+ companion object {
+ private val inboxTopic = "${P2P_PREFIX}test"
+ }
+
+ @Test
+ fun `Basic function tests`() {
+ val conf = rigorousMock().also {
+ doReturn(ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.toList()).whenever(it).whitelistedHeaders
+ }
+ val auditService = TestAuditService()
+ val dummyMessage = rigorousMock().also {
+ doReturn(it).whenever(it).putStringProperty(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
+ doReturn(it).whenever(it).putStringProperty(ArgumentMatchers.any(), ArgumentMatchers.any())
+ doReturn(it).whenever(it).writeBodyBufferBytes(any())
+ }
+ val dummyProducer = rigorousMock().also {
+ doNothing().whenever(it).send(any(), eq(dummyMessage), any())
+ doNothing().whenever(it).close()
+ }
+ val dummySession = rigorousMock().also {
+ doReturn(dummyMessage).whenever(it).createMessage(true)
+ doReturn(dummyProducer).whenever(it).createProducer()
+ doNothing().whenever(it).close()
+ }
+ val artemisStarted = ArtemisMessagingClient.Started(
+ rigorousMock(),
+ rigorousMock().also {
+ doReturn(dummySession).whenever(it).createSession(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
+ },
+ rigorousMock(),
+ rigorousMock()
+ )
+ val artemisService = createPartialMock().also {
+ doReturn(artemisStarted).whenever(it).started
+ }
+ val senderService = createPartialMock().also {
+ doReturn(true).whenever(it).validateReceiveTopic(ArgumentMatchers.anyString(), any())
+ }
+ val filterService = SimpleMessageFilterService(conf, auditService, artemisService, senderService)
+ val stateFollower = filterService.activeChange.toBlocking().iterator
+ val auditFollower = auditService.onAuditEvent.toBlocking().iterator
+ filterService.start()
+ // Not ready so packet dropped
+ val fakeMessage = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK'd
+ }
+ filterService.sendMessageToLocalBroker(fakeMessage)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next()) // Dropped as not ready
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, filterService.active)
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ auditService.start()
+ assertEquals(false, filterService.active)
+ artemisService.start()
+ assertEquals(false, filterService.active)
+ senderService.start()
+ assertEquals(true, stateFollower.next())
+ assertEquals(true, filterService.active)
+ // ready so packet forwarded
+ val goodMessage = rigorousMock().also {
+ doNothing().whenever(it).complete(true) // ACK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(goodMessage)
+ assertEquals(TestAuditService.AuditEvent.PACKET_ACCEPT, auditFollower.next()) // Accepted the message
+ verify(dummyProducer, times(1)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // message forwarded
+ filterService.stop()
+ assertEquals(false, stateFollower.next())
+ assertEquals(false, filterService.active)
+ }
+
+
+ @Test
+ fun `Rejection tests`() {
+ val conf = rigorousMock().also {
+ doReturn(ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.toList()).whenever(it).whitelistedHeaders
+ }
+ val auditService = TestAuditService()
+ val dummyMessage = rigorousMock().also {
+ doReturn(it).whenever(it).putStringProperty(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
+ doReturn(it).whenever(it).putStringProperty(ArgumentMatchers.any(), ArgumentMatchers.any())
+ doReturn(it).whenever(it).writeBodyBufferBytes(any())
+ }
+ val dummyProducer = rigorousMock().also {
+ doNothing().whenever(it).send(any(), eq(dummyMessage), any())
+ doNothing().whenever(it).close()
+ }
+ val dummySession = rigorousMock().also {
+ doReturn(dummyMessage).whenever(it).createMessage(true)
+ doReturn(dummyProducer).whenever(it).createProducer()
+ doNothing().whenever(it).close()
+ }
+ val artemisStarted = ArtemisMessagingClient.Started(
+ rigorousMock(),
+ rigorousMock().also {
+ doReturn(dummySession).whenever(it).createSession(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
+ },
+ rigorousMock(),
+ rigorousMock()
+ )
+ val artemisService = createPartialMock().also {
+ doReturn(artemisStarted).whenever(it).started
+ }
+ val senderService = createPartialMock().also {
+ doAnswer {
+ val topic = it.arguments[0] as String
+ (topic == inboxTopic)
+ }.whenever(it).validateReceiveTopic(ArgumentMatchers.anyString(), any())
+ }
+ val filterService = SimpleMessageFilterService(conf, auditService, artemisService, senderService)
+ val auditFollower = auditService.onAuditEvent.toBlocking().iterator
+ auditService.start()
+ artemisService.start()
+ senderService.start()
+ filterService.start()
+ assertEquals(true, filterService.active)
+
+ // empty legal name
+ val badMessage1 = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK was called
+ doReturn("").whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(badMessage1)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next())
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ // bad legal name
+ val badMessage2 = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK was called
+ doReturn("CN=Test").whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(badMessage2)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next())
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ // empty payload
+ val badMessage3 = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(0)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(badMessage3)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next())
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ // bad topic
+ val badMessage4 = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn("bridge.control").whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(badMessage4)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next())
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ // Non-whitelist header header
+ val badMessage5 = rigorousMock().also {
+ doNothing().whenever(it).complete(false) // NAK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(mapOf("Suspicious" to "Header")).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(badMessage5)
+ assertEquals(TestAuditService.AuditEvent.PACKET_DROP, auditFollower.next())
+ verify(dummyProducer, times(0)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+
+ // Valid message sent and completed
+ val goodMessage = rigorousMock().also {
+ doNothing().whenever(it).complete(true) // ACK was called
+ doReturn(DUMMY_BANK_B_NAME.toString()).whenever(it).sourceLegalName
+ doReturn(inboxTopic).whenever(it).topic
+ doReturn(ByteArray(1)).whenever(it).payload
+ doReturn(emptyMap()).whenever(it).applicationProperties
+ }
+ filterService.sendMessageToLocalBroker(goodMessage)
+ assertEquals(TestAuditService.AuditEvent.PACKET_ACCEPT, auditFollower.next()) // packet was accepted
+ verify(dummyProducer, times(1)).send(ArgumentMatchers.any(), eq(dummyMessage), ArgumentMatchers.any()) // not sent
+ filterService.stop()
+ }
+
+}
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/services/TestAuditService.kt b/bridge/src/test/kotlin/net/corda/bridge/services/TestAuditService.kt
new file mode 100644
index 0000000000..f355519e34
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/services/TestAuditService.kt
@@ -0,0 +1,49 @@
+package net.corda.bridge.services
+
+import net.corda.bridge.services.api.BridgeAuditService
+import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
+import rx.Observable
+import rx.subjects.PublishSubject
+import java.net.InetSocketAddress
+
+class TestAuditService() : BridgeAuditService, TestServiceBase() {
+ enum class AuditEvent {
+ SUCCESSFUL_CONNECTION,
+ FAILED_CONNECTION,
+ PACKET_DROP,
+ PACKET_ACCEPT,
+ STATUS_CHANGE
+ }
+
+ var eventCount: Int = 0
+ private set
+
+ private val _onAuditEvent = PublishSubject.create().toSerialized()
+ val onAuditEvent: Observable
+ get() = _onAuditEvent
+
+ override fun successfulConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress, certificateSubject: String, msg: String) {
+ ++eventCount
+ _onAuditEvent.onNext(AuditEvent.SUCCESSFUL_CONNECTION)
+ }
+
+ override fun failedConnectionEvent(inbound: Boolean, sourceIP: InetSocketAddress?, certificateSubject: String?, msg: String) {
+ ++eventCount
+ _onAuditEvent.onNext(AuditEvent.FAILED_CONNECTION)
+ }
+
+ override fun packetDropEvent(packet: ReceivedMessage?, msg: String) {
+ ++eventCount
+ _onAuditEvent.onNext(AuditEvent.PACKET_DROP)
+ }
+
+ override fun packetAcceptedEvent(packet: ReceivedMessage) {
+ ++eventCount
+ _onAuditEvent.onNext(AuditEvent.PACKET_ACCEPT)
+ }
+
+ override fun statusChangeEvent(msg: String) {
+ ++eventCount
+ _onAuditEvent.onNext(AuditEvent.STATUS_CHANGE)
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/test/kotlin/net/corda/bridge/services/TestServiceBase.kt b/bridge/src/test/kotlin/net/corda/bridge/services/TestServiceBase.kt
new file mode 100644
index 0000000000..afb2372f9c
--- /dev/null
+++ b/bridge/src/test/kotlin/net/corda/bridge/services/TestServiceBase.kt
@@ -0,0 +1,24 @@
+package net.corda.bridge.services
+
+import net.corda.bridge.services.api.ServiceLifecycleSupport
+import net.corda.bridge.services.util.ServiceStateHelper
+import org.slf4j.helpers.NOPLogger
+import rx.Observable
+
+open class TestServiceBase() : ServiceLifecycleSupport {
+ private val stateHelper: ServiceStateHelper = ServiceStateHelper(NOPLogger.NOP_LOGGER)
+
+ override val active: Boolean
+ get() = stateHelper.active
+
+ override val activeChange: Observable
+ get() = stateHelper.activeChange
+
+ override fun start() {
+ stateHelper.active = true
+ }
+
+ override fun stop() {
+ stateHelper.active = false
+ }
+}
\ No newline at end of file
diff --git a/bridge/src/test/resources/net/corda/bridge/singleprocess/bridge.conf b/bridge/src/test/resources/net/corda/bridge/singleprocess/bridge.conf
new file mode 100644
index 0000000000..48034c1f7e
--- /dev/null
+++ b/bridge/src/test/resources/net/corda/bridge/singleprocess/bridge.conf
@@ -0,0 +1,8 @@
+bridgeMode = SenderReceiver
+outboundConfig : {
+ artemisBrokerAddress = "localhost:11005"
+}
+inboundConfig : {
+ listeningAddress = "0.0.0.0:10005"
+}
+networkParametersPath = network-parameters
diff --git a/bridge/src/test/resources/net/corda/bridge/withfloat/bridge/bridge.conf b/bridge/src/test/resources/net/corda/bridge/withfloat/bridge/bridge.conf
new file mode 100644
index 0000000000..e3764fcc32
--- /dev/null
+++ b/bridge/src/test/resources/net/corda/bridge/withfloat/bridge/bridge.conf
@@ -0,0 +1,9 @@
+bridgeMode = FloatInner
+outboundConfig : {
+ artemisBrokerAddress = "localhost:11005"
+}
+floatInnerConfig : {
+ floatAddresses = [ "localhost:12005" ]
+ expectedCertificateSubject = "O=Bank A, L=London, C=GB"
+}
+networkParametersPath = network-parameters
diff --git a/bridge/src/test/resources/net/corda/bridge/withfloat/float/bridge.conf b/bridge/src/test/resources/net/corda/bridge/withfloat/float/bridge.conf
new file mode 100644
index 0000000000..c7bd5f67aa
--- /dev/null
+++ b/bridge/src/test/resources/net/corda/bridge/withfloat/float/bridge.conf
@@ -0,0 +1,9 @@
+bridgeMode = FloatOuter
+inboundConfig : {
+ listeningAddress = "0.0.0.0:10005"
+}
+floatOuterConfig : {
+ floatAddress = "localhost:12005"
+ expectedCertificateSubject = "O=Bank A, L=London, C=GB"
+}
+networkParametersPath = network-parameters
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt
index 60b49505bc..4b1d3ce03a 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt
@@ -33,7 +33,7 @@ class ArtemisMessagingClient(
private val autoCommitSends: Boolean = true,
private val autoCommitAcks: Boolean = true,
private val confirmationWindowSize: Int = -1
-): ArtemisSessionProvider {
+) : ArtemisSessionProvider {
companion object {
private val log = loggerFor()
}
diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
index cdd057dcdd..074429b628 100644
--- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
@@ -59,8 +59,6 @@ class AMQPBridgeTest {
private abstract class AbstractNodeConfiguration : NodeConfiguration
- // TODO: revisit upon Matthew Nesbitt return
- @Ignore()
@Test
fun `test acked and nacked messages`() {
// Create local queue
diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt
index 5680707bfa..8d8022513a 100644
--- a/node/src/main/kotlin/net/corda/node/internal/Node.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt
@@ -178,9 +178,11 @@ open class Node(configuration: NodeConfiguration,
} else {
startLocalRpcBroker(networkParameters)
}
- val advertisedAddress = info.addresses[0]
- bridgeControlListener = BridgeControlListener(configuration, serverAddress, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE)
-
+ val advertisedAddress = info.addresses.single()
+ val externalBridge = configuration.enterpriseConfiguration.externalBridge
+ if (externalBridge == null || !externalBridge) {
+ bridgeControlListener = BridgeControlListener(configuration, serverAddress, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE)
+ }
printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
val rpcServerConfiguration = RPCServerConfiguration.default.copy(
@@ -205,6 +207,7 @@ open class Node(configuration: NodeConfiguration,
database,
services.networkMapCache,
services.monitoringService.metrics,
+ info.legalIdentities[0].name.toString(),
advertisedAddress,
/*networkParameters.maxMessageSize*/MAX_FILE_SIZE,
nodeProperties.flowsDrainingMode::isEnabled,
diff --git a/node/src/main/kotlin/net/corda/node/services/config/EnterpriseConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/EnterpriseConfiguration.kt
index e357ac1eed..5bca510f3a 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/EnterpriseConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/EnterpriseConfiguration.kt
@@ -16,8 +16,8 @@ import net.corda.node.services.statemachine.transitions.StateMachineConfiguratio
data class EnterpriseConfiguration(
val mutualExclusionConfiguration: MutualExclusionConfiguration,
val useMultiThreadedSMM: Boolean = true,
- val tuning: PerformanceTuning = PerformanceTuning.default
-)
+ val tuning: PerformanceTuning = PerformanceTuning.default,
+ val externalBridge: Boolean? = null)
data class MutualExclusionConfiguration(val on: Boolean = false, val machineName: String, val updateInterval: Long, val waitInterval: Long)
diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
index a0b023b35a..1bc76f20db 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
@@ -48,7 +48,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
val verifierType: VerifierType
val messageRedeliveryDelaySeconds: Int
val notary: NotaryConfig?
- val activeMQServer: ActiveMqServerConfiguration
val additionalNodeInfoPollingFrequencyMsec: Long
val p2pAddress: NetworkHostAndPort
val rpcOptions: NodeRpcOptions
@@ -139,12 +138,6 @@ data class BFTSMaRtConfiguration(
}
}
-data class BridgeConfiguration(val retryIntervalMs: Long,
- val maxRetryIntervalMin: Long,
- val retryIntervalMultiplier: Double)
-
-data class ActiveMqServerConfiguration(val bridge: BridgeConfiguration)
-
fun Config.parseAsNodeConfiguration(): NodeConfiguration = parseAs()
data class NodeConfigurationImpl(
@@ -178,7 +171,6 @@ data class NodeConfigurationImpl(
override val devModeOptions: DevModeOptions? = null,
override val useTestClock: Boolean = false,
override val detectPublicIp: Boolean = true,
- override val activeMQServer: ActiveMqServerConfiguration,
// TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(),
override val sshd: SSHDConfiguration? = null,
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
index 713e0838b3..e21e16092c 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
@@ -35,7 +35,6 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt b/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt
index 0da1beccd4..62ccababa6 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/MessagingExecutor.kt
@@ -49,7 +49,8 @@ class MessagingExecutor(
val resolver: AddressToArtemisQueueResolver,
metricRegistry: MetricRegistry,
val ourSenderUUID: String,
- queueBound: Int
+ queueBound: Int,
+ val myLegalName: String
) {
private sealed class Job {
data class Acknowledge(val message: ClientMessage) : Job()
@@ -164,6 +165,7 @@ class MessagingExecutor(
putStringProperty(P2PMessagingHeaders.releaseVersionProperty, releaseVersion)
putIntProperty(P2PMessagingHeaders.platformVersionProperty, versionInfo.platformVersion)
putStringProperty(P2PMessagingHeaders.topicProperty, SimpleString(message.topic))
+ putStringProperty(P2PMessagingHeaders.bridgedCertificateSubject, SimpleString(myLegalName))
sendMessageSizeMetric.update(message.data.bytes.size)
writeBodyBufferBytes(message.data.bytes)
// Use the magic deduplication property built into Artemis as our message identity too
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
index 3cdc52f58d..97e7eca293 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
@@ -106,6 +106,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
private val database: CordaPersistence,
private val networkMap: NetworkMapCacheInternal,
private val metricRegistry: MetricRegistry,
+ val legalName: String,
advertisedAddress: NetworkHostAndPort = serverAddress,
private val maxMessageSize: Int,
private val isDrainingModeOn: () -> Boolean,
@@ -170,6 +171,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
private val messageRedeliveryDelaySeconds = config.messageRedeliveryDelaySeconds.toLong()
private val state = ThreadBox(InnerState())
private val knownQueues = Collections.newSetFromMap(ConcurrentHashMap())
+ private val externalBridge: Boolean = config.enterpriseConfiguration.externalBridge ?: false
private val handlers = ConcurrentHashMap()
@@ -238,7 +240,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
this@P2PMessagingClient,
metricRegistry,
queueBound = config.enterpriseConfiguration.tuning.maximumMessagingBatchSize,
- ourSenderUUID = deduplicator.ourSenderUUID
+ ourSenderUUID = deduplicator.ourSenderUUID,
+ myLegalName = legalName
)
this@P2PMessagingClient.messagingExecutor = messagingExecutor
messagingExecutor.start()
@@ -384,7 +387,12 @@ class P2PMessagingClient(val config: NodeConfiguration,
private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? {
try {
val topic = message.required(P2PMessagingHeaders.topicProperty) { getStringProperty(it) }
- val user = requireNotNull(message.getStringProperty(HDR_VALIDATED_USER)) { "Message is not authenticated" }
+ val user = requireNotNull(if (externalBridge) {
+ message.getStringProperty(P2PMessagingHeaders.bridgedCertificateSubject) ?: message.getStringProperty(HDR_VALIDATED_USER)
+ } else {
+ message.getStringProperty(HDR_VALIDATED_USER)
+ }) { "Message is not authenticated" }
+
val platformVersion = message.required(P2PMessagingHeaders.platformVersionProperty) { getIntProperty(it) }
// Use the magic deduplication property built into Artemis as our message identity too
val uniqueMessageId = message.required(HDR_DUPLICATE_DETECTION_ID) { DeduplicationId(message.getStringProperty(it)) }
diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf
index e7fbcfc57b..a0cf277f42 100644
--- a/node/src/main/resources/reference.conf
+++ b/node/src/main/resources/reference.conf
@@ -16,13 +16,6 @@ devMode = true
h2port = 0
useTestClock = false
verifierType = InMemory
-activeMQServer = {
- bridge = {
- retryIntervalMs = 5000
- retryIntervalMultiplier = 1.5
- maxRetryIntervalMin = 3
- }
-}
enterpriseConfiguration = {
mutualExclusionConfiguration = {
on = false
diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
index fd24f48ceb..1255d15b47 100644
--- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
@@ -118,7 +118,6 @@ class NodeConfigurationImplTest {
certificateChainCheckPolicies = emptyList(),
devMode = true,
noLocalShell = false,
- activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0)),
rpcSettings = rpcSettings,
relay = null,
enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000)))
diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt
index 1c14d4f9a3..261b6fc4fa 100644
--- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt
@@ -216,10 +216,10 @@ class ArtemisMessagingTest {
// Now change the receiver
try {
val messagingClient2 = createMessagingClient()
- messagingClient2.addMessageHandler(TOPIC) { message, _, handle ->
+ messagingClient2.addMessageHandler(TOPIC) { msg, _, handle ->
database.transaction { handle.persistDeduplicationId() }
handle.acknowledge() // We ACK first so that if it fails we won't get a duplicate in [receivedMessages]
- receivedMessages.add(message)
+ receivedMessages.add(msg)
}
startNodeMessagingClient()
@@ -251,10 +251,10 @@ class ArtemisMessagingTest {
fakeMsg2!!.putStringProperty(HDR_VALIDATED_USER, SimpleString("O=Bank A, L=New York, C=US"))
val messagingClient3 = createMessagingClient()
- messagingClient3.addMessageHandler(TOPIC) { message, _, handle ->
+ messagingClient3.addMessageHandler(TOPIC) { msg, _, handle ->
database.transaction { handle.persistDeduplicationId() }
handle.acknowledge() // We ACK first so that if it fails we won't get a duplicate in [receivedMessages]
- receivedMessages.add(message)
+ receivedMessages.add(msg)
}
startNodeMessagingClient()
@@ -305,6 +305,7 @@ class ArtemisMessagingTest {
database,
networkMapCache,
MetricRegistry(),
+ ALICE_NAME.toString(),
maxMessageSize = maxMessageSize,
isDrainingModeOn = { false },
drainingModeWasChangedEvents = PublishSubject.create()).apply {
diff --git a/settings.gradle b/settings.gradle
index d579647f04..7a75e81a0a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,6 +19,8 @@ include 'docs'
include 'node-api'
include 'node'
include 'node:capsule'
+include 'bridge'
+include 'bridge:bridgecapsule'
include 'client:jackson'
include 'client:jfx'
include 'client:mock'