From 1ae4c20b10b29dbef1a9fe619f35ab5a54ca2e1e Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 3 May 2018 14:24:27 +0100 Subject: [PATCH] Slim down bridge capsule jar Add bridge smoke-test to :bridge:bridgecapsule to verify that all the dependencies are correctly packaged and present. Correct proton-j version Correct proton-j version --- bridge/bridgecapsule/build.gradle | 51 ++++ .../corda/bridge/smoketest/BridgeSmokeTest.kt | 241 ++++++++++++++++++ .../net/corda/bridge/smoketest/bridge.conf | 26 ++ bridge/build.gradle | 37 ++- build.gradle | 1 + node-api/build.gradle | 2 +- 6 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 bridge/bridgecapsule/src/smoke-test/kotlin/net/corda/bridge/smoketest/BridgeSmokeTest.kt create mode 100644 bridge/bridgecapsule/src/smoke-test/resources/net/corda/bridge/smoketest/bridge.conf diff --git a/bridge/bridgecapsule/build.gradle b/bridge/bridgecapsule/build.gradle index f68a3986cf..302ef05216 100644 --- a/bridge/bridgecapsule/build.gradle +++ b/bridge/bridgecapsule/build.gradle @@ -10,6 +10,7 @@ * 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: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' apply plugin: 'com.jfrog.artifactory' @@ -19,11 +20,48 @@ description 'Corda bridge server capsule' configurations { runtimeArtifacts capsuleRuntime + + smokeTestCompile.extendsFrom testCompile + smokeTestRuntime.extendsFrom testRuntime +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +sourceSets { + smokeTest { + kotlin { + // We must NOT have any Bridge code on the classpath, so do NOT + // include the test or integrationTest dependencies here. + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/smoke-test/kotlin') + } + resources { + srcDir file('src/smoke-test/resources') + } + } } dependencies { // TypeSafe Config: for simple and human friendly config files. capsuleRuntime "com.typesafe:config:$typesafe_config_version" + + + // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + smokeTestCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" + smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + smokeTestCompile "org.assertj:assertj-core:${assertj_version}" + smokeTestCompile project(':node-driver') + smokeTestCompile project(':test-utils') + smokeTestCompile "org.apache.curator:curator-test:${curator_version}" + smokeTestCompile "junit:junit:$junit_version" + } // Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards @@ -65,6 +103,19 @@ task buildBridgeServerJar(type: FatCapsule, dependsOn: project(':bridge').jar) { } } +processSmokeTestResources { + + from(project.tasks['buildBridgeServerJar']) { + rename 'corda-bridgeserver-(.*)', 'corda-bridgeserver.jar' + into "net/corda/bridge/smoketest" + } +} + +task smokeTest(type: Test) { + testClassesDirs = sourceSets.smokeTest.output.classesDirs + classpath = sourceSets.smokeTest.runtimeClasspath +} + artifacts { runtimeArtifacts buildBridgeServerJar publish buildBridgeServerJar { diff --git a/bridge/bridgecapsule/src/smoke-test/kotlin/net/corda/bridge/smoketest/BridgeSmokeTest.kt b/bridge/bridgecapsule/src/smoke-test/kotlin/net/corda/bridge/smoketest/BridgeSmokeTest.kt new file mode 100644 index 0000000000..cfb7e1b824 --- /dev/null +++ b/bridge/bridgecapsule/src/smoke-test/kotlin/net/corda/bridge/smoketest/BridgeSmokeTest.kt @@ -0,0 +1,241 @@ +package net.corda.bridge.smoketest + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.* +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.node.services.AttachmentId +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +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.* +import net.corda.nodeapi.internal.bridging.BridgeControl +import net.corda.nodeapi.internal.config.NodeSSLConfiguration +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.* +import net.corda.testing.internal.rigorousMock +import org.apache.activemq.artemis.api.core.RoutingType +import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.curator.test.TestingServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.net.Socket +import java.nio.file.Path +import java.nio.file.Paths +import java.security.cert.X509Certificate +import java.time.Instant +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.streams.toList + +class BridgeSmokeTest { + companion object { + val log = contextLogger() + } + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Rule + @JvmField + val serializationEnvironment = SerializationEnvironmentRule(true) + + private abstract class AbstractNodeConfiguration : NodeConfiguration + + @Before + fun setup() { + + } + + @After + fun cleanup() { + + } + + @Test + fun `Run full features bridge from jar to ensure everything works`() { + val artemisConfig = object : NodeSSLConfiguration { + override val baseDirectory: Path = tempFolder.root.toPath() + override val keyStorePassword: String = "cordacadevpass" + override val trustStorePassword: String = "trustpass" + override val crlCheckSoftFail: Boolean = true + } + artemisConfig.createBridgeKeyStores(DUMMY_BANK_A_NAME) + copyBridgeResource("corda-bridgeserver.jar") + copyBridgeResource("bridge.conf") + createNetworkParams(tempFolder.root.toPath()) + val (artemisServer, artemisClient) = createArtemis() + val zkServer = TestingServer(11105, false) + try { + installBridgeControlResponder(artemisClient) + zkServer.start() + val bridge = startBridge(tempFolder.root.toPath()) + waitForBridge(bridge) + } finally { + zkServer.close() + artemisClient.stop() + artemisServer.stop() + } + + } + + private fun copyBridgeResource(resourceName: String) { + val testDir = tempFolder.root.toPath() + // Find the finance jar file for the smoke tests of this module + val bridgeJar = Paths.get("build", "resources/smokeTest/net/corda/bridge/smoketest").list { + it.filter { resourceName in it.toString() }.toList().single() + } + bridgeJar.copyToDirectory(testDir) + } + + 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 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 = Crypto.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)) + } + } + + private fun startBridge(baseDirectory: Path): Process { + val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") + val builder = ProcessBuilder() + .command(javaPath.toString(), "-Dcapsule.log=verbose", + "-jar", "corda-bridgeserver.jar") + .directory(baseDirectory.toFile()) + .inheritIO() + + builder.environment().putAll(mapOf( + "CAPSULE_CACHE_DIR" to (baseDirectory / "capsule").toString() + )) + + log.info("Start bridge process in $baseDirectory") + return builder.start() + } + + private fun waitForBridge(process: Process) { + var ok = false + val executor = Executors.newSingleThreadScheduledExecutor() + try { + executor.scheduleWithFixedDelay({ + try { + if (!process.isAlive) { + log.error("Bridge has died.") + return@scheduleWithFixedDelay + } + if (!serverListening("localhost", 10005)) { + log.warn("Bridge not listening yet") + return@scheduleWithFixedDelay + } + + ok = true + + // Cancel the polling + executor.shutdown() + } catch (e: Exception) { + log.warn("Bridge not ready yet (Error: {})", e.message) + } + }, 5, 1, TimeUnit.SECONDS) + + val setupOK = executor.awaitTermination(60, TimeUnit.SECONDS) + check(setupOK && ok && process.isAlive) { "Bridge Failed to open listening port" } + } catch (e: Exception) { + throw e + } finally { + executor.shutdownNow() + process.destroyForcibly() + } + } + + 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) { + } + } + } + + 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, NetworkHostAndPort("0.0.0.0", 11005), MAX_MESSAGE_SIZE) + val artemisClient = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", 11005), MAX_MESSAGE_SIZE) + artemisServer.start() + artemisClient.start() + return Pair(artemisServer, artemisClient) + } + + private fun installBridgeControlResponder(artemisClient: ArtemisMessagingClient) { + val artemis = artemisClient.started!! + val inboxAddress = SimpleString("${ArtemisMessagingComponent.P2P_PREFIX}Test") + artemis.session.createQueue(inboxAddress, RoutingType.ANYCAST, inboxAddress, true) + artemis.session.createQueue(ArtemisMessagingComponent.BRIDGE_NOTIFY, RoutingType.ANYCAST, ArtemisMessagingComponent.BRIDGE_NOTIFY, false) + val controlConsumer = artemis.session.createConsumer(ArtemisMessagingComponent.BRIDGE_NOTIFY) + controlConsumer.setMessageHandler { msg -> + val bridgeControl = BridgeControl.NodeToBridgeSnapshot("Test", listOf(inboxAddress.toString()), emptyList()) + val controlPacket = bridgeControl.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes + val artemisMessage = artemis.session.createMessage(false) + artemisMessage.writeBodyBufferBytes(controlPacket) + artemis.producer.send(ArtemisMessagingComponent.BRIDGE_CONTROL, artemisMessage) + msg.acknowledge() + } + } +} \ No newline at end of file diff --git a/bridge/bridgecapsule/src/smoke-test/resources/net/corda/bridge/smoketest/bridge.conf b/bridge/bridgecapsule/src/smoke-test/resources/net/corda/bridge/smoketest/bridge.conf new file mode 100644 index 0000000000..e65f5eec02 --- /dev/null +++ b/bridge/bridgecapsule/src/smoke-test/resources/net/corda/bridge/smoketest/bridge.conf @@ -0,0 +1,26 @@ +// +// R3 Proprietary and Confidential +// +// Copyright (c) 2018 R3 Limited. All rights reserved. +// +// The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. +// +// Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. + +bridgeMode = SenderReceiver +outboundConfig : { + artemisBrokerAddress = "localhost:11005" + socksProxyConfig : { + version = SOCKS5 + proxyAddress = "localhost:12345" + userName = "proxyUser" + password = "pwd" + } +} +inboundConfig : { + listeningAddress = "0.0.0.0:10005" +} +haConfig : { + haConnectionString = "zk://localhost:11105" +} +networkParametersPath = network-parameters \ No newline at end of file diff --git a/bridge/build.gradle b/bridge/build.gradle index 1f718264ee..12c5286605 100644 --- a/bridge/build.gradle +++ b/bridge/build.gradle @@ -34,7 +34,41 @@ processResources { } dependencies { - compile project(':node-api') + + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + + compile(project(':core')) { + transitive = false // we control dependencies directly as the bridge is likely to be audited + } + + compile(project(':node-api')) { + transitive = false// we control dependencies directly as the bridge is likely to be audited + } + + // Here we pull in dependencies that would normally be pulled in transitively from :core and :node-api, but we need more fine grained control + // For AMQP serialisation. + compile "org.apache.qpid:proton-j:${protonj_version}" + // RxJava: observable streams of events. + compile "io.reactivex:rxjava:$rxjava_version" + compile("org.apache.activemq:artemis-core-client:${artemis_version}") + compile "org.apache.activemq:artemis-commons:${artemis_version}" + // Netty: All of it! Dn't depend upon ActiveMQ to pull it in correctly + compile "io.netty:netty-all:$netty_version" + // TypeSafe Config: for simple and human friendly config files. + compile "com.typesafe:config:$typesafe_config_version" + + // The following dependencies are required to load and deserialize network-info, or other static initializers that get triggered. + // Java ed25519 implementation. See https://github.com/str4d/ed25519-java/ for deserializing our public keys and certs. + compile "net.i2p.crypto:eddsa:$eddsa_version" + // Bouncy castle support needed for X509 certificate manipulation + compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" + compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" + // Seems to be needed? + compile "com.github.ben-manes.caffeine:caffeine:$caffeine_version" + // Pulled in by whitelist + compile "com.esotericsoftware:kryo:4.0.0" // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" @@ -50,6 +84,7 @@ dependencies { integrationTestCompile project(':node-driver') integrationTestCompile "org.apache.curator:curator-test:${curator_version}" testCompile "junit:junit:$junit_version" + testCompile "org.apache.curator:curator-test:${curator_version}" testCompile project(':test-utils') } diff --git a/build.gradle b/build.gradle index d1aaa2a74a..a5a955b62b 100644 --- a/build.gradle +++ b/build.gradle @@ -93,6 +93,7 @@ buildscript { ext.eaagentloader_version = '1.0.3' ext.curator_version = '4.0.0' ext.jsch_version = '0.1.54' + ext.protonj_version = '0.27.1' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' diff --git a/node-api/build.gradle b/node-api/build.gradle index 9a2d8b3657..ff408cfc6a 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -44,7 +44,7 @@ dependencies { compile "de.javakaffee:kryo-serializers:0.41" // For AMQP serialisation. - compile "org.apache.qpid:proton-j:0.27.1" + compile "org.apache.qpid:proton-j:${protonj_version}" // SQL connection pooling library compile "com.zaxxer:HikariCP:$hikari_version"