From e47e9d546dd63bbd858960608ea0d324651b02ad Mon Sep 17 00:00:00 2001 From: Joseph Zuniga-Daly <59851625+josephzunigadaly@users.noreply.github.com> Date: Tue, 3 Mar 2020 16:06:46 +0000 Subject: [PATCH 01/22] Fix the operator used by the notEqual predicate (#6022) * Fix the operator used by the notEqual predicate * Add timeouts to tests * Add change to changelog --- .../node/services/vault/QueryCriteriaUtils.kt | 2 +- docs/source/changelog.rst | 2 + .../vault/QueryCriteriaUtilsBuilderTest.kt | 165 ++++++++++++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index a698038242..70983a7056 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -303,7 +303,7 @@ object Builder { @JvmStatic @JvmOverloads - fun FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + fun FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch)) @JvmStatic @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b05887084a..7b77cb085f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -83,6 +83,8 @@ Unreleased * :doc:`design/data-model-upgrades/package-namespace-ownership` configurations can be now be set as described in :ref:`node_package_namespace_ownership`, when using the Cordformation plugin version 4.0.43. +* Fixed the operator used by the ``notEqual`` predicate + .. _changelog_v4.1: Version 4.1 diff --git a/node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt new file mode 100644 index 0000000000..646a539762 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt @@ -0,0 +1,165 @@ +package net.corda.node.services.vault + +import net.corda.core.node.services.vault.BinaryComparisonOperator +import net.corda.core.node.services.vault.Builder.`in` +import net.corda.core.node.services.vault.Builder.equal +import net.corda.core.node.services.vault.Builder.greaterThan +import net.corda.core.node.services.vault.Builder.greaterThanOrEqual +import net.corda.core.node.services.vault.Builder.isNull +import net.corda.core.node.services.vault.Builder.lessThan +import net.corda.core.node.services.vault.Builder.lessThanOrEqual +import net.corda.core.node.services.vault.Builder.like +import net.corda.core.node.services.vault.Builder.notEqual +import net.corda.core.node.services.vault.Builder.notIn +import net.corda.core.node.services.vault.Builder.notLike +import net.corda.core.node.services.vault.Builder.notNull +import net.corda.core.node.services.vault.CollectionOperator +import net.corda.core.node.services.vault.ColumnPredicate +import net.corda.core.node.services.vault.ColumnPredicate.AggregateFunction +import net.corda.core.node.services.vault.ColumnPredicate.Between +import net.corda.core.node.services.vault.ColumnPredicate.BinaryComparison +import net.corda.core.node.services.vault.ColumnPredicate.CollectionExpression +import net.corda.core.node.services.vault.ColumnPredicate.EqualityComparison +import net.corda.core.node.services.vault.ColumnPredicate.Likeness +import net.corda.core.node.services.vault.ColumnPredicate.NullExpression +import net.corda.core.node.services.vault.CriteriaExpression.ColumnPredicateExpression +import net.corda.core.node.services.vault.EqualityComparisonOperator +import net.corda.core.node.services.vault.FieldInfo +import net.corda.core.node.services.vault.LikenessOperator +import net.corda.core.node.services.vault.NullOperator +import net.corda.core.node.services.vault.Operator +import net.corda.core.node.services.vault.getField +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ObjectAssert +import org.junit.Test +import javax.persistence.Entity + +class QueryCriteriaUtilsBuilderTest { + + /** JPA Entity class needed by `getField` */ + @Entity + private class TestEntity(val field: String) + + /** Returns a `FieldInfo` object to work on */ + private val fieldInfo: FieldInfo get() = getField("field", TestEntity::class.java) + + /** Thrown for the `ColumnPredicate` types that have no `operator` field */ + private class ColumnPredicateHasNoOperatorFieldException : Exception("This ColumnPredicate has no operator field") + + /** Returns the `operator` for the given `ColumnPredicate` */ + private fun ColumnPredicate.getOperator(): Operator = when (this) { + is AggregateFunction -> throw ColumnPredicateHasNoOperatorFieldException() + is Between -> throw ColumnPredicateHasNoOperatorFieldException() + is BinaryComparison<*> -> operator + is CollectionExpression -> operator + is EqualityComparison<*> -> operator + is Likeness -> operator + is NullExpression -> operator + } + + /** Returns the `operator` for the given `ColumnPredicateExpression` */ + private fun ColumnPredicateExpression.getOperator(): Operator = this.predicate.getOperator() + + /** Assert that the `ColumnPredicateExpression` uses the given `Operator`. */ + private fun , C> ObjectAssert.usesOperator(operator: Operator) { + extracting { + assertThat(it.getOperator()).isEqualTo(operator) + } + } + + /** Sample `String` value to pass to the predicate expression */ + private val stringValue = "" + + /** Sample `List` value to pass to the predicate expression */ + private val listValue = emptyList() + + @Test(timeout = 500) + fun `equal predicate uses EQUAL operator`() { + assertThat(fieldInfo.equal(stringValue)).usesOperator(EqualityComparisonOperator.EQUAL) + } + + @Test(timeout = 500) + fun `equal predicate (exactMatch=false) uses EQUAL_IGNORE_CASE operator`() { + assertThat(fieldInfo.equal(stringValue, exactMatch = false)).usesOperator(EqualityComparisonOperator.EQUAL_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `notEqual predicate uses NOT_EQUAL operator`() { + assertThat(fieldInfo.notEqual(stringValue)).usesOperator(EqualityComparisonOperator.NOT_EQUAL) + } + + @Test(timeout = 500) + fun `notEqual predicate (exactMatch=false) uses NOT_EQUAL_IGNORE_CASE operator`() { + assertThat(fieldInfo.notEqual(stringValue, exactMatch = false)).usesOperator(EqualityComparisonOperator.NOT_EQUAL_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `lessThan predicate uses LESS_THAN operator`() { + assertThat(fieldInfo.lessThan(stringValue)).usesOperator(BinaryComparisonOperator.LESS_THAN) + } + + @Test(timeout = 500) + fun `lessThanOrEqual predicate uses LESS_THAN_OR_EQUAL operator`() { + assertThat(fieldInfo.lessThanOrEqual(stringValue)).usesOperator(BinaryComparisonOperator.LESS_THAN_OR_EQUAL) + } + + @Test(timeout = 500) + fun `greaterThan predicate uses GREATER_THAN operator`() { + assertThat(fieldInfo.greaterThan(stringValue)).usesOperator(BinaryComparisonOperator.GREATER_THAN) + } + + @Test(timeout = 500) + fun `greaterThanOrEqual predicate uses GREATER_THAN_OR_EQUAL operator`() { + assertThat(fieldInfo.greaterThanOrEqual(stringValue)).usesOperator(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL) + } + + @Test(timeout = 500) + fun `in predicate uses IN operator`() { + assertThat(fieldInfo.`in`(listValue)).usesOperator(CollectionOperator.IN) + } + + @Test(timeout = 500) + fun `in predicate (exactMatch=false) uses IN_IGNORE_CASE operator`() { + assertThat(fieldInfo.`in`(listValue, exactMatch = false)).usesOperator(CollectionOperator.IN_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `notIn predicate uses NOT_IN operator`() { + assertThat(fieldInfo.notIn(listValue)).usesOperator(CollectionOperator.NOT_IN) + } + + @Test(timeout = 500) + fun `notIn predicate (exactMatch=false) uses NOT_IN_IGNORE_CASE operator`() { + assertThat(fieldInfo.notIn(listValue, exactMatch = false)).usesOperator(CollectionOperator.NOT_IN_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `like predicate uses LIKE operator`() { + assertThat(fieldInfo.like(stringValue)).usesOperator(LikenessOperator.LIKE) + } + + @Test(timeout = 500) + fun `like predicate (exactMatch=false) uses LIKE_IGNORE_CASE operator`() { + assertThat(fieldInfo.like(stringValue, exactMatch = false)).usesOperator(LikenessOperator.LIKE_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `notLike predicate uses NOT_LIKE operator`() { + assertThat(fieldInfo.notLike(stringValue)).usesOperator(LikenessOperator.NOT_LIKE) + } + + @Test(timeout = 500) + fun `notLike predicate (exactMatch=false) uses NOT_LIKE_IGNORE_CASE operator`() { + assertThat(fieldInfo.notLike(stringValue, exactMatch = false)).usesOperator(LikenessOperator.NOT_LIKE_IGNORE_CASE) + } + + @Test(timeout = 500) + fun `isNull predicate uses IS_NULL operator`() { + assertThat(fieldInfo.isNull()).usesOperator(NullOperator.IS_NULL) + } + + @Test(timeout = 500) + fun `notNull predicate uses NOT_NULL operator`() { + assertThat(fieldInfo.notNull()).usesOperator(NullOperator.NOT_NULL) + } +} From b02768aa6e51f56993a6db04b27e6b1b0f72aa84 Mon Sep 17 00:00:00 2001 From: Joseph Zuniga-Daly Date: Mon, 9 Mar 2020 15:08:40 +0000 Subject: [PATCH 02/22] CORDA-3394: Move unit test to correct location --- .../core}/node/services/vault/QueryCriteriaUtilsBuilderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {node/src/test/kotlin/net/corda => core/src/test/kotlin/net/corda/core}/node/services/vault/QueryCriteriaUtilsBuilderTest.kt (99%) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt b/core/src/test/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtilsBuilderTest.kt similarity index 99% rename from node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt rename to core/src/test/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtilsBuilderTest.kt index 646a539762..86b6dfcea0 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/QueryCriteriaUtilsBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtilsBuilderTest.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.vault +package net.corda.core.node.services.vault import net.corda.core.node.services.vault.BinaryComparisonOperator import net.corda.core.node.services.vault.Builder.`in` From 3973fe46f250af045e17f2b3413e8668f1c1e1ff Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 9 Mar 2020 15:11:53 +0000 Subject: [PATCH 03/22] Update contributing-philosophy.rst (#6044) --- docs/source/contributing-philosophy.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/source/contributing-philosophy.rst b/docs/source/contributing-philosophy.rst index 8d1eeab10d..dbed0dc099 100644 --- a/docs/source/contributing-philosophy.rst +++ b/docs/source/contributing-philosophy.rst @@ -11,8 +11,7 @@ modern business transactions. It is unique in its aim to build a platform for b retaining strict privacy. Corda provides an implementation of this vision in a code base which others are free to build on, contribute to or innovate around. The mission of Corda is further detailed in the `Corda introductory white paper`_. -The project is supported and maintained by the `R3 Alliance `_, or R3 for short, which consists of over two hundred firms -working together to build and maintain this open source enterprise-grade blockchain platform. +The project is supported and maintained by `R3 `_. Community Locations ------------------- @@ -44,12 +43,12 @@ Community maintainers ^^^^^^^^^^^^^^^^^^^^^ Current community maintainers: -* `Joel Dudley `_ - Contact me: +* `Rick Parker `_ - Contact me: * On the `Corda Slack team `_, either in the ``#community`` channel or by direct message using the handle - ``@joel`` + ``@parkri`` - * By email: joel.dudley at r3.com + * By email: rick.parker at r3.com We anticipate additional maintainers joining the project in the future from across the community. @@ -60,13 +59,12 @@ Over two hundred individuals have contributed to the development of Corda. You c Transparency and Conflict Policy -------------------------------- -The project is supported and maintained by the `R3 Alliance `_, which consists of over two hundred firms working together -to build and maintain this open source enterprise-grade blockchain platform. We develop in the open and publish our +The project is supported and maintained by `R3 `_. We develop in the open and publish our `Jira `_ to give everyone visibility. R3 also maintains and distributes a commercial distribution of Corda. Our vision is that distributions of Corda be compatible and interoperable, and our contribution and code review guidelines are designed in part to enable this. -As the R3 Alliance is maintainer of the project and also develops a commercial distribution of Corda, what happens if a member of the +As R3 is maintainer of the project and also develops a commercial distribution of Corda, what happens if a member of the community contributes a feature which the R3 team have implemented only in their commercial product? How is this apparent conflict managed? Our approach is simple: if the contribution meets the standards for the project (see above), then the existence of a competing commercial implementation will not be used as a reason to reject it. In other words, it is our policy that should a community feature be contributed From ab6bf315c495340ef2f315586b0a25f94c8d655f Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Tue, 10 Mar 2020 12:36:30 +0000 Subject: [PATCH 04/22] ENT-4494 Harmonize CryptoService interface/base class between OS and ENT, move OS BC implementation to node project. --- .../corda/nodeapi/internal/cryptoservice/CryptoService.kt | 5 ----- .../internal/cryptoservice/bouncycastle/BCCryptoService.kt | 3 --- node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt | 4 ++-- .../node/utilities}/cryptoservice/CryptoServiceFactory.kt | 3 ++- .../node/utilities}/cryptoservice/SupportedCryptoServices.kt | 2 +- .../node/utilities/registration/NetworkRegistrationHelper.kt | 4 ++-- .../net/corda/testing/node/internal/MockCryptoService.kt | 3 --- 7 files changed, 7 insertions(+), 17 deletions(-) rename {node-api/src/main/kotlin/net/corda/nodeapi/internal => node/src/main/kotlin/net/corda/node/utilities}/cryptoservice/CryptoServiceFactory.kt (89%) rename {node-api/src/main/kotlin/net/corda/nodeapi/internal => node/src/main/kotlin/net/corda/node/utilities}/cryptoservice/SupportedCryptoServices.kt (75%) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt index f7fe302cc1..0eea108376 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt @@ -61,11 +61,6 @@ interface CryptoService : SignOnlyCryptoService { */ fun generateKeyPair(alias: String, scheme: SignatureScheme): PublicKey - /** - * Returns the type of the service. - */ - fun getType(): SupportedCryptoServices - // ****************************************************** // ENTERPRISE ONLY CODE FOR WRAPPING KEYS API STARTS HERE diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt index 7cd65378b7..8fcdbb2bfd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt @@ -17,7 +17,6 @@ import net.corda.nodeapi.internal.crypto.save import net.corda.nodeapi.internal.cryptoservice.* import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException -import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices import org.bouncycastle.operator.ContentSigner import java.nio.file.Path import java.security.* @@ -41,8 +40,6 @@ class BCCryptoService(private val legalName: X500Principal, val detailedLogger = detailedLogger() } - override fun getType(): SupportedCryptoServices = SupportedCryptoServices.BC_SIMPLE - // TODO check if keyStore exists. // TODO make it private when E2ETestKeyManagementService does not require direct access to the private key. var certificateStore: CertificateStore = certificateStoreSupplier.get(true) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 22bc371d6f..3141b71bbc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -150,8 +150,8 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS -import net.corda.nodeapi.internal.cryptoservice.CryptoServiceFactory -import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices +import net.corda.node.utilities.cryptoservice.CryptoServiceFactory +import net.corda.node.utilities.cryptoservice.SupportedCryptoServices import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoServiceFactory.kt b/node/src/main/kotlin/net/corda/node/utilities/cryptoservice/CryptoServiceFactory.kt similarity index 89% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoServiceFactory.kt rename to node/src/main/kotlin/net/corda/node/utilities/cryptoservice/CryptoServiceFactory.kt index 0d7d3abb44..2a633bf61a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoServiceFactory.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/cryptoservice/CryptoServiceFactory.kt @@ -1,7 +1,8 @@ -package net.corda.nodeapi.internal.cryptoservice +package net.corda.node.utilities.cryptoservice import net.corda.core.identity.CordaX500Name import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService class CryptoServiceFactory { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/SupportedCryptoServices.kt b/node/src/main/kotlin/net/corda/node/utilities/cryptoservice/SupportedCryptoServices.kt similarity index 75% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/SupportedCryptoServices.kt rename to node/src/main/kotlin/net/corda/node/utilities/cryptoservice/SupportedCryptoServices.kt index 58c1c6320e..9c5b6fe4b2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/SupportedCryptoServices.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/cryptoservice/SupportedCryptoServices.kt @@ -1,4 +1,4 @@ -package net.corda.nodeapi.internal.cryptoservice +package net.corda.node.utilities.cryptoservice enum class SupportedCryptoServices(val userFriendlyName: String) { /** Identifier for [BCCryptoService]. */ diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 417012ba3e..08678c16c8 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -18,8 +18,8 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW import net.corda.nodeapi.internal.cryptoservice.CryptoService -import net.corda.nodeapi.internal.cryptoservice.CryptoServiceFactory -import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices +import net.corda.node.utilities.cryptoservice.CryptoServiceFactory +import net.corda.node.utilities.cryptoservice.SupportedCryptoServices import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.openssl.jcajce.JcaPEMWriter diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt index 629eed988a..741c5d2126 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt @@ -10,7 +10,6 @@ import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.cryptoservice.* import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException -import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey @@ -22,8 +21,6 @@ import javax.crypto.SecretKey class MockCryptoService(initialKeyPairs: Map) : CryptoService { - override fun getType(): SupportedCryptoServices = SupportedCryptoServices.BC_SIMPLE - private val aliasToKey: MutableMap = mutableMapOf() private val wrappingKeys: MutableMap = mutableMapOf() From 53d92771bb0c380c79457ac3e6633c96c8fd1d5f Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Tue, 10 Mar 2020 14:21:40 +0000 Subject: [PATCH 05/22] ENT-4494 Harmonize configuration classes. --- .../internal/config/CertificateStore.kt | 1 + .../internal/config/ConfigUtilities.kt | 97 +++++++++++++++---- .../internal/config/SslConfiguration.kt | 9 +- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt index 3cecaa97c8..b5285c93cd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt @@ -60,6 +60,7 @@ interface CertificateStore : Iterable> { forEach { (alias, certificate) -> action.invoke(alias, certificate) } } + fun aliases(): List = value.internal.aliases().toList() /** * @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate]. */ diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index f9f2e0bde9..56f12e0dde 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("ConfigUtilities") +@file:Suppress("LongParameterList") package net.corda.nodeapi.internal.config @@ -52,21 +53,35 @@ const val CUSTOM_NODE_PROPERTIES_ROOT = "custom" // This is to enable constructs like: // `val keyStorePassword: String by config` operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { - return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle, nestedPath = null, baseDirectory = null) + return getValueInternal( + metadata.name, + metadata.returnType, + UnknownConfigKeysPolicy.IGNORE::handle, + nestedPath = null, + baseDirectory = null + ) } // Problems: // - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure. // - Encourages weak bean-like types. -// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files. +// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for +// versioning of the configuration files. // - It's complicated and based on reflection, meaning problems with it are typically found at runtime. -// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once. -fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, - nestedPath: String? = null, baseDirectory: Path? = null): T { +// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good +// usability practices like displaying all the errors at once. +fun Config.parseAs( + clazz: KClass, + onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, + nestedPath: String? = null, + baseDirectory: Path? = null +): T { // Use custom parser if provided, instead of treating the object as data class. clazz.findAnnotation()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) } - require(clazz.isData) { "Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}" } + require(clazz.isData) { + "Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}" + } val constructor = clazz.primaryConstructor!! val parameters = constructor.parameters val parameterNames = parameters.flatMap { param -> @@ -104,24 +119,46 @@ class UnknownConfigurationKeysException private constructor(val unknownKeys: Set companion object { fun of(offendingKeys: Set): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys) - private fun message(offendingKeys: Set) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}." + private fun message(offendingKeys: Set) = "Unknown configuration keys: " + + "${offendingKeys.joinToString(", ", "[", "]")}." } } -inline fun Config.parseAs(noinline onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T = parseAs(T::class, onUnknownKeys) +inline fun Config.parseAs( + noinline onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle +): T = parseAs(T::class, onUnknownKeys) fun Config.toProperties(): Properties { return entrySet().associateByTo( Properties(), { ConfigUtil.splitPath(it.key).joinToString(".") }, - { it.value.unwrapped().toString() }) + { it.value.unwrapped() }) } -private fun Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set, logger: Logger) -> Unit), nestedPath: String?, baseDirectory: Path?): T { - return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory) else getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory)) +private fun Config.getValueInternal( + path: String, + type: KType, + onUnknownKeys: ((Set, logger: Logger) -> Unit), + nestedPath: String?, + baseDirectory: Path? +): T { + return uncheckedCast( + if (type.arguments.isEmpty()) { + getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory) + } else { + getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory) + } + ) } -private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? { +@Suppress("ComplexMethod") +private fun Config.getSingleValue( + path: String, + type: KType, + onUnknownKeys: (Set, logger: Logger) -> Unit, + nestedPath: String?, + baseDirectory: Path? +): Any? { if (type.isMarkedNullable && !hasPath(path)) return null val typeClass = type.jvmErasure return try { @@ -153,7 +190,12 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { - getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path, baseDirectory = baseDirectory) + getConfig(path).parseAs( + typeClass, + onUnknownKeys, + nestedPath?.let { "$it.$path" } ?: path, + baseDirectory = baseDirectory + ) } } } catch (e: ConfigException.Missing) { @@ -164,7 +206,8 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path { val path = Paths.get(pathAsString) return if (baseDirectory != null) { - // if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction has no effect. + // if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction + // has no effect. baseDirectory.resolve(path) } else { path @@ -178,10 +221,18 @@ private fun ConfigException.Missing.relative(path: String, nestedPath: String?): } } -private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection { +@Suppress("ComplexMethod") +private fun Config.getCollectionValue( + path: String, + type: KType, + onUnknownKeys: (Set, logger: Logger) -> Unit, + nestedPath: String?, + baseDirectory: Path? +): Collection { val typeClass = type.jvmErasure require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" } - val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type") + val elementClass = type.arguments[0].type?.jvmErasure + ?: throw IllegalArgumentException("Cannot work with star projection: $type") if (!hasPath(path)) { return if (typeClass == List::class) emptyList() else emptySet() } @@ -240,7 +291,13 @@ private fun > enumBridge(clazz: Class, name: String): T { */ fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig() -fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) this else if (this != null) ConfigValueFactory.fromAnyRef(convertValue(this)) else ConfigValueFactory.fromAnyRef(null) +fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) { + this +} else if (this != null) { + ConfigValueFactory.fromAnyRef(convertValue(this)) +} else { + ConfigValueFactory.fromAnyRef(null) +} @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Reflect over the fields of the receiver and generate a value Map that can use to create Config object. @@ -253,7 +310,8 @@ private fun Any.toConfigMap(): Map { val configValue = if (value is String || value is Boolean || value is Number) { // These types are supported by Config as use as is value - } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) { + } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || + value is Path || value is URL || value is UUID || value is X500Principal) { // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs value.toString() } else if (value is Enum<*>) { @@ -278,7 +336,8 @@ private fun convertValue(value: Any): Any { return if (value is String || value is Boolean || value is Number) { // These types are supported by Config as use as is value - } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) { + } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || + value is Path || value is URL || value is UUID || value is X500Principal) { // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs value.toString() } else if (value is Enum<*>) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt index 1c5a90e4bf..e4433b4e00 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt @@ -4,6 +4,7 @@ interface SslConfiguration { val keyStore: FileBasedCertificateStoreSupplier? val trustStore: FileBasedCertificateStoreSupplier? + val useOpenSsl: Boolean companion object { @@ -19,4 +20,10 @@ interface MutualSslConfiguration : SslConfiguration { override val trustStore: FileBasedCertificateStoreSupplier } -private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration \ No newline at end of file +private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, + override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration { + override val useOpenSsl: Boolean = false +} + +const val DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS = 60000L // Set at least 3 times higher than sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT which is 15 sec + From d61536c6ffa1c47bf28accea249a09aed8f786bb Mon Sep 17 00:00:00 2001 From: VCAMP Date: Wed, 11 Mar 2020 11:56:46 +0000 Subject: [PATCH 06/22] OS 4.4 release notes: added warning about integration testing changes (#6025) * OS 4.4 release notes: added warning about integration testing changes * OS 4.4 release notes: more datail on integration testing changes --- docs/source/release-notes.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 67291c790e..029f89a10b 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -51,6 +51,26 @@ Security enhancements * The ability to SSH into the standalone shell has been removed * A new read-only RPC user role template has been documented in :doc:`shell` +Changes to integration testing ++++++++++++++++++++++++ + +The "out-of-process" nodes spawned through Driver DSL (see :doc:`tutorial-integration-testing`) will no longer accidentally contain your CorDapps on their application classpath. The list of items that will be automatically filtered out include: + +* Directories (only regular files are allowed) +* Jars with Maven classifiers ``tests`` or ``test`` +* Jars with any Cordapp attributes in their manifests (any of those listed in :doc:`cordapp-build-systems` or ``Target-Platform-Version`` and ``Min-Platform-Version`` if both are present) +* Jars with the ``Corda-Testing`` attribute in their manifests. The manifest of the following artifacts has been updated to include the ``Corda-Testing`` attribute: + + * ``corda-node-driver`` + * ``corda-test-utils`` + * ``corda-test-common`` + * ``corda-test-db`` + * ``corda-mock`` + +* Files whose names start with ``corda-mock``, ``junit``, ``testng`` or ``mockito`` + +Some of your existing integration tests might implicitly be relying on the presence of the above files, so please keep this in mind when upgrading your version of Corda. + Platform version change ~~~~~~~~~~~~~~~~~~~~~~~ From c565d47adcc2fecc78a845ba6d7c9de85e52f9c5 Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Thu, 12 Mar 2020 08:15:28 +0000 Subject: [PATCH 07/22] use proxied gradle to prevent slow lorrising due to too many requests (#6056) * use proxied gradle to prevent slow lorrising due to too many requests * fix typo --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4483da647f..2e954d6bc7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Wed Aug 21 10:48:19 BST 2019 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.4.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists From 8973c5ba961e1bafed081c253d77932658f2f1e6 Mon Sep 17 00:00:00 2001 From: Razvan Codreanu <52859362+Schife@users.noreply.github.com> Date: Thu, 12 Mar 2020 10:04:22 +0000 Subject: [PATCH 08/22] TM-168 reusable configurations for distributed testing (#6048) * TM-168 reusable configurations for distributed testing * TM-168 switching to class * TM-168 switching to ext properties * TM-168 large scale set is too aggressive and prone to failure so switching to general purpose --- build.gradle | 80 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/build.gradle b/build.gradle index d4e1cdee29..0772c6740a 100644 --- a/build.gradle +++ b/build.gradle @@ -633,61 +633,77 @@ buildScan { termsOfServiceAgree = 'yes' } +ext.generalPurpose = [ + numberOfShards: 10, + streamOutput: false, + coresPerFork: 2, + memoryInGbPerFork: 12, + nodeTaints: "small" +] + +ext.largeScaleSet = [ + numberOfShards: 15, + streamOutput: false, + coresPerFork: 6, + memoryInGbPerFork: 10, + nodeTaints: "big" +] + task allParallelIntegrationTest(type: ParallelTestGroup) { podLogLevel PodLogLevel.INFO testGroups "integrationTest" - numberOfShards 10 - streamOutput false - coresPerFork 2 - memoryInGbPerFork 12 + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints distribute DistributeTestsBy.METHOD - nodeTaints "big" } task allParallelUnitTest(type: ParallelTestGroup) { podLogLevel PodLogLevel.INFO testGroups "test" - numberOfShards 10 - streamOutput false - coresPerFork 2 - memoryInGbPerFork 12 + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints distribute DistributeTestsBy.CLASS - nodeTaints "small" } task allParallelUnitAndIntegrationTest(type: ParallelTestGroup) { testGroups "test", "integrationTest" - numberOfShards 15 - streamOutput false - coresPerFork 6 - memoryInGbPerFork 10 + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints distribute DistributeTestsBy.METHOD - nodeTaints "big" } task parallelRegressionTest(type: ParallelTestGroup) { testGroups "test", "integrationTest", "smokeTest" - numberOfShards 15 - streamOutput false - coresPerFork 2 - memoryInGbPerFork 10 + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints distribute DistributeTestsBy.METHOD - nodeTaints "big" } task allParallelSmokeTest(type: ParallelTestGroup) { testGroups "smokeTest" - numberOfShards 4 - streamOutput false - coresPerFork 6 - memoryInGbPerFork 10 - distribute DistributeTestsBy.CLASS - nodeTaints "big" + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints + distribute DistributeTestsBy.METHOD } task allParallelSlowIntegrationTest(type: ParallelTestGroup) { testGroups "slowIntegrationTest" - numberOfShards 4 - streamOutput false - coresPerFork 6 - memoryInGbPerFork 10 - distribute DistributeTestsBy.CLASS - nodeTaints "big" + numberOfShards generalPurpose.numberOfShards + streamOutput generalPurpose.streamOutput + coresPerFork generalPurpose.coresPerFork + memoryInGbPerFork generalPurpose.memoryInGbPerFork + nodeTaints generalPurpose.nodeTaints + distribute DistributeTestsBy.METHOD } apply plugin: 'com.r3.testing.distributed-testing' apply plugin: 'com.r3.testing.image-building' From 3b335ebb006518bf3a2d89ede853932594ca7e4a Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Thu, 12 Mar 2020 12:48:40 +0000 Subject: [PATCH 09/22] NOTICK Fix statemachine error handling tests (#6057) Due to a change in how messaging works, `ActionExecutorImpl .executeSendInitial` was no longer being called. Changing the byteman script to throw exception on hits to `ActionExecutorImpl .executeSendMultiple` allowed the tests to pass. --- .../StatemachineGeneralErrorHandlingTest.kt | 12 ++++++------ .../StatemachineKillFlowErrorHandlingTest.kt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt index 8308328827..5aacac8a4a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineGeneralErrorHandlingTest.kt @@ -34,15 +34,15 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") @@ -114,15 +114,15 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 3 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt index 6e4f7bf2d8..ea2319ee88 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StatemachineKillFlowErrorHandlingTest.kt @@ -208,15 +208,15 @@ class StatemachineKillFlowErrorHandlingTest : StatemachineErrorHandlingTest() { val rules = """ RULE Create Counter CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF createCounter("counter", $counter) DO traceln("Counter created") ENDRULE - RULE Throw exception on executeSendInitial action + RULE Throw exception on executeSendMultiple action CLASS ${ActionExecutorImpl::class.java.name} - METHOD executeSendInitial + METHOD executeSendMultiple AT ENTRY IF readCounter("counter") < 4 DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die") From 723399d3a17052eadf15a7024b2ff6c4bbdc071c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 13 Mar 2020 08:58:33 +0000 Subject: [PATCH 10/22] NOTICK: Prevent learning the location of a Class inside sandbox. --- core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 3d170290b5..c71462e33c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -4,6 +4,7 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM +import net.corda.core.StubOutForDJVM import net.corda.core.crypto.Crypto import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash @@ -417,6 +418,7 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } /** Returns the location of this class. */ +@get:StubOutForDJVM val Class<*>.location: URL get() = protectionDomain.codeSource.location /** Convenience method to get the package name of a class literal. */ From ea22a10b3e96dc73cffac942c8adc3483d6cdf73 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 13 Mar 2020 14:26:24 +0000 Subject: [PATCH 11/22] ENT-4494 Harmonize network stack (#6059) * ENT-4494 harmonize proton wrapper with ENT * Harmonise Artemis and Bridge implementation * Move tests across * detekt changes * Fix AMQP tests in node --- detekt-baseline.xml | 41 +- node-api/build.gradle | 2 + .../internal/ArtemisMessagingClient.kt | 47 ++- .../internal/ArtemisMessagingComponent.kt | 6 + .../nodeapi/internal/ArtemisTcpTransport.kt | 31 +- .../corda/nodeapi/internal/ArtemisUtils.kt | 2 +- .../nodeapi/internal/ClientSessionUtils.kt | 8 + .../corda/nodeapi/internal/ConcurrentBox.kt | 17 + .../internal/RoundRobinConnectionPolicy.kt | 18 + .../internal/bridging/AMQPBridgeManager.kt | 395 ++++++++++++++---- .../bridging/BridgeControlListener.kt | 205 +++++++-- .../bridging/BridgeControlMessages.kt | 11 +- .../internal/bridging/BridgeManager.kt | 7 +- .../bridging/LoopbackBridgeManager.kt | 223 ++++++++++ .../MessagingServerConnectionConfiguration.kt | 63 +++ .../engine/ConnectionStateMachine.kt | 11 +- .../protonwrapper/engine/EventProcessor.kt | 4 +- .../messages/ApplicationMessage.kt | 1 + .../messages/impl/ReceivedMessageImpl.kt | 19 +- .../messages/impl/SendableMessageImpl.kt | 10 +- .../protonwrapper/netty/AMQPChannelHandler.kt | 218 +++++++--- .../protonwrapper/netty/AMQPClient.kt | 183 ++++++-- .../protonwrapper/netty/AMQPConfiguration.kt | 45 +- .../protonwrapper/netty/AMQPServer.kt | 75 +++- .../netty/AliasProvidingKeyMangerWrapper.kt | 60 +++ .../netty/AllowAllRevocationChecker.kt | 34 ++ .../CertHoldingKeyManagerFactoryWrapper.kt | 81 ++++ .../protonwrapper/netty/ExternalCrlSource.kt | 12 + .../netty/ModeSelectingChannel.kt | 76 ++++ .../netty/NettyServerEventLogger.kt | 73 ++++ .../protonwrapper/netty/RevocationConfig.kt | 83 ++++ .../protonwrapper/netty/SNIKeyManager.kt | 112 +++++ .../internal/protonwrapper/netty/SSLHelper.kt | 230 ++++++++-- .../protonwrapper/netty/ServerSNIMatcher.kt | 47 +++ .../netty/TrustManagerFactoryWrapper.kt | 40 ++ .../ExternalSourceRevocationChecker.kt | 88 ++++ .../internal/config/ConfigParsingTest.kt | 1 - .../protonwrapper/netty/SSLHelperTest.kt | 15 +- .../ExternalSourceRevocationCheckerTest.kt | 56 +++ .../internal/protonwrapper/netty/Readme.txt | 3 + .../internal/protonwrapper/netty/doorman.crl | Bin 0 -> 576 bytes .../netty/sslkeystore_Revoked.jks | Bin 0 -> 3594 bytes .../serialization/amqp/networkParamsWrite | Bin 0 -> 3066 bytes .../net/corda/node/amqp/AMQPBridgeTest.kt | 16 +- .../CertificateRevocationListNodeTests.kt | 8 +- .../net/corda/node/amqp/ProtonWrapperTests.kt | 11 +- .../kotlin/net/corda/node/internal/Node.kt | 11 +- .../services/messaging/P2PMessagingClient.kt | 13 +- 48 files changed, 2372 insertions(+), 340 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/ClientSessionUtils.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/ConcurrentBox.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/RoundRobinConnectionPolicy.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/config/MessagingServerConnectionConfiguration.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AliasProvidingKeyMangerWrapper.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AllowAllRevocationChecker.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/CertHoldingKeyManagerFactoryWrapper.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ExternalCrlSource.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ModeSelectingChannel.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/NettyServerEventLogger.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/RevocationConfig.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SNIKeyManager.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ServerSNIMatcher.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/TrustManagerFactoryWrapper.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/revocation/ExternalSourceRevocationChecker.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/revocation/ExternalSourceRevocationCheckerTest.kt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/Readme.txt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/networkParamsWrite diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 3e147108a6..20c1cb21e4 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -91,7 +91,6 @@ ComplexCondition:WireTransaction.kt$WireTransaction$notary != null && (inputs.isNotEmpty() || references.isNotEmpty() || timeWindow != null) ComplexMethod:AMQPBridgeManager.kt$AMQPBridgeManager.AMQPBridge$private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) ComplexMethod:AMQPBridgeTest.kt$AMQPBridgeTest$@Test(timeout=300_000) fun `test acked and nacked messages`() - ComplexMethod:AMQPChannelHandler.kt$AMQPChannelHandler$override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) ComplexMethod:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String) ComplexMethod:ANSIProgressRenderer.kt$ANSIProgressRenderer$// Returns number of lines rendered. private fun renderLevel(ansi: Ansi, error: Boolean): Int ComplexMethod:ANSIProgressRenderer.kt$ANSIProgressRenderer$@Synchronized protected fun draw(moveUp: Boolean, error: Throwable? = null) @@ -124,13 +123,10 @@ ComplexMethod:ConfigUtilities.kt$// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. private fun Iterable<*>.toConfigIterable(field: Field): Iterable<Any?> ComplexMethod:ConfigUtilities.kt$// TODO Move this to KeyStoreConfigHelpers. fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path, cryptoService: CryptoService? = null) ComplexMethod:ConfigUtilities.kt$@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Reflect over the fields of the receiver and generate a value Map that can use to create Config object. private fun Any.toConfigMap(): Map<String, Any> - ComplexMethod:ConfigUtilities.kt$private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection<Any> - ComplexMethod:ConfigUtilities.kt$private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? ComplexMethod:ConfigUtilities.kt$private fun convertValue(value: Any): Any ComplexMethod:ConnectionStateMachine.kt$ConnectionStateMachine$override fun onConnectionFinal(event: Event) ComplexMethod:ConnectionStateMachine.kt$ConnectionStateMachine$override fun onDelivery(event: Event) ComplexMethod:ConstraintsUtils.kt$ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean - ComplexMethod:CordaCliWrapper.kt$fun CordaCliWrapper.start(args: Array<String>) ComplexMethod:CordaPersistence.kt$CordaPersistence$private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -> T): T ComplexMethod:CordaRPCClient.kt$CordaRPCClientConfiguration$override fun equals(other: Any?): Boolean ComplexMethod:CordaRPCClientTest.kt$CordaRPCClientTest$@Test(timeout=300_000) fun `shutdown command stops the node`() @@ -182,7 +178,6 @@ ComplexMethod:RPCClientProxyHandler.kt$RPCClientProxyHandler$// This is the general function that transforms a client side RPC to internal Artemis messages. override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): Any? ComplexMethod:RPCClientProxyHandler.kt$RPCClientProxyHandler$private fun attemptReconnect() ComplexMethod:RPCServer.kt$RPCServer$private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) - ComplexMethod:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ErrorInterceptingHandler$ private fun doInvoke(method: Method, args: Array<out Any>?, maxNumberOfAttempts: Int): Any? ComplexMethod:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ReconnectingRPCConnection$ private tailrec fun establishConnectionWithRetry( retryInterval: Duration, roundRobinIndex: Int = 0, retries: Int = -1 ): CordaRPCConnection? ComplexMethod:RemoteTypeCarpenter.kt$SchemaBuildingRemoteTypeCarpenter$override fun carpent(typeInformation: RemoteTypeInformation): Type ComplexMethod:RpcReconnectTests.kt$RpcReconnectTests$ @Test(timeout=300_000) fun `test that the RPC client is able to reconnect and proceed after node failure, restart, or connection reset`() @@ -215,7 +210,6 @@ EmptyDefaultConstructor:FlowRetryTest.kt$AsyncRetryFlow$() EmptyDefaultConstructor:FlowRetryTest.kt$RetryFlow$() EmptyDefaultConstructor:FlowRetryTest.kt$ThrowingFlow$() - EmptyElseBlock:CordaCliWrapper.kt${ } EmptyIfBlock:ContentSignerBuilder.kt$ContentSignerBuilder.SignatureOutputStream$if (alreadySigned) throw IllegalStateException("Cannot write to already signed object") EmptyIfBlock:InMemoryIdentityService.kt$InMemoryIdentityService${ } EmptyKtFile:KryoHook.kt$.KryoHook.kt @@ -331,7 +325,6 @@ ForbiddenComment:DigitalSignatureWithCert.kt$// TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl$// TODO: Derive name from the full picked name, don't just wrap the common name ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl$//TODO: remove this once we can bundle quasar properly. - ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl.Companion$// TODO: There is pending work to fix this issue without custom blacklisting. See: https://r3-cev.atlassian.net/browse/CORDA-2164. ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl.LocalNetworkMap$// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ ForbiddenComment:DummyFungibleContract.kt$DummyFungibleContract$// TODO: This doesn't work with the trader demo, so use the underlying key instead ForbiddenComment:E2ETestKeyManagementService.kt$E2ETestKeyManagementService$// TODO: A full KeyManagementService implementation needs to record activity to the Audit Service and to limit @@ -388,6 +381,7 @@ ForbiddenComment:LegalNameValidator.kt$LegalNameValidator.Rule.Companion$// TODO: Implement confusable character detection if we add more scripts. ForbiddenComment:LocalTypeInformationBuilder.kt$// TODO: Revisit this when Kotlin issue is fixed. ForbiddenComment:LoggingBuyerFlow.kt$LoggingBuyerFlow$// TODO: This is potentially very expensive, and requires transaction details we may no longer have once + ForbiddenComment:LoopbackBridgeManager.kt$LoopbackBridgeManager.LoopbackBridge$// TODO: refactor MDC support, duplicated in AMQPBridgeManager. ForbiddenComment:MockServices.kt$MockServices.Companion$// TODO: Can we use an X509 principal generator here? ForbiddenComment:NetParams.kt$NetParamsSigner$// TODO: not supported ForbiddenComment:NetworkBootstrapper.kt$NetworkBootstrapper$// TODO: pass a commandline parameter to the bootstrapper instead. Better yet, a notary config map @@ -630,8 +624,6 @@ FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_no_metadata() FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_no_value() FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_present() - FunctionNaming:VersionedParsingExampleTest.kt$VersionedParsingExampleTest$@Test(timeout=300_000) fun correct_parsing_function_is_used_for_present_version() - FunctionNaming:VersionedParsingExampleTest.kt$VersionedParsingExampleTest$@Test(timeout=300_000) fun default_value_is_used_for_absent_version() LargeClass:AbstractNode.kt$AbstractNode<S> : SingletonSerializeAsToken LargeClass:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager : StateMachineManagerStateMachineManagerInternal LongMethod:FlowCookbook.kt$InitiatorFlow$@Suppress("RemoveExplicitTypeArguments") @Suspendable override fun call() @@ -655,7 +647,6 @@ LongParameterList:CertificateRevocationListNodeTests.kt$CertificateRevocationListNodeTests$(port: Int, name: CordaX500Name = ALICE_NAME, crlCheckSoftFail: Boolean, nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE) LongParameterList:CertificateRevocationListNodeTests.kt$CertificateRevocationListNodeTests.Companion$(clrServer: CrlServer, signatureAlgorithm: String, caCertificate: X509Certificate, caPrivateKey: PrivateKey, endpoint: String, indirect: Boolean, vararg serialNumbers: BigInteger) LongParameterList:CertificateStoreStubs.kt$CertificateStoreStubs.P2P.Companion$(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD) - LongParameterList:CertificateStoreStubs.kt$CertificateStoreStubs.P2P.Companion$(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD, trustStoreKeyPassword: String = TrustStore.DEFAULT_KEY_PASSWORD, @Suppress("UNUSED_PARAMETER") useOpenSsl: Boolean = false) LongParameterList:ContractAttachment.kt$ContractAttachment.Companion$(attachment: Attachment, contract: ContractClassName, additionalContracts: Set<ContractClassName> = emptySet(), uploader: String? = null, signerKeys: List<PublicKey> = emptyList(), version: Int = DEFAULT_CORDAPP_VERSION) LongParameterList:ContractFunctions.kt$(expiry: String, notional: BigDecimal, strike: BigDecimal, foreignCurrency: Currency, domesticCurrency: Currency, partyA: Party, partyB: Party) LongParameterList:ContractFunctions.kt$(expiry: String, notional: Long, strike: Double, foreignCurrency: Currency, domesticCurrency: Currency, partyA: Party, partyB: Party) @@ -925,7 +916,6 @@ MagicNumber:IrsDemoWebApplication.kt$IrsDemoWebApplication$1000 MagicNumber:JarScanningCordappLoader.kt$CordappLoaderTemplate$36 MagicNumber:JarScanningCordappLoader.kt$CordappLoaderTemplate$64 - MagicNumber:JarScanningCordappLoader.kt$JarScanningCordappLoader$1000 MagicNumber:JarSignatureCollector.kt$JarSignatureCollector$1024 MagicNumber:JarSignatureTestUtils.kt$JarSignatureTestUtils$14 MagicNumber:KMSUtils.kt$3650 @@ -1229,8 +1219,6 @@ MatchingDeclarationName:Query.kt$net.corda.webserver.api.Query.kt MatchingDeclarationName:ReceiveAllFlowTests.kt$net.corda.coretests.flows.ReceiveAllFlowTests.kt MatchingDeclarationName:ReferenceInputStateTests.kt$net.corda.coretests.transactions.ReferenceInputStateTests.kt - MatchingDeclarationName:RigorousMock.kt$net.corda.testing.internal.RigorousMock.kt - MatchingDeclarationName:RpcServerCordaFutureSerialiser.kt$net.corda.node.serialization.amqp.RpcServerCordaFutureSerialiser.kt MatchingDeclarationName:SSLHelper.kt$net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper.kt MatchingDeclarationName:SampleData.kt$net.corda.deterministic.verifier.SampleData.kt MatchingDeclarationName:SerializationHelper.kt$net.corda.networkbuilder.serialization.SerializationHelper.kt @@ -1285,7 +1273,6 @@ NestedBlockDepth:RPCClientProxyHandler.kt$RPCClientProxyHandler$// The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) NestedBlockDepth:ShutdownManager.kt$ShutdownManager$fun shutdown() NestedBlockDepth:SpringDriver.kt$SpringBootDriverDSL$private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle - NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map<SessionId, SerializedBytes<Any>>) NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction) NestedBlockDepth:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) @@ -1316,7 +1303,6 @@ SpreadOperator:ConfigUtilities.kt$(*pairs) SpreadOperator:Configuration.kt$Configuration.Validation.Error$(*(containingPath.toList() + this.containingPath).toTypedArray()) SpreadOperator:ContractJarTestUtils.kt$ContractJarTestUtils$(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray()) - SpreadOperator:CordaCliWrapper.kt$(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(defaultAnsiMode).andExit(ExitCodes.FAILURE), *args) SpreadOperator:CordaRPCOpsImpl.kt$CordaRPCOpsImpl$(logicType, context(), *args) SpreadOperator:CordaX500Name.kt$CordaX500Name.Companion$(*Locale.getISOCountries(), unspecifiedCountry) SpreadOperator:CustomCordapp.kt$CustomCordapp$(*classes.map { it.name }.toTypedArray()) @@ -1326,7 +1312,6 @@ SpreadOperator:DockerInstantiator.kt$DockerInstantiator$(*it.toTypedArray()) SpreadOperator:DummyContract.kt$DummyContract.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owner.owningKey), /* OUTPUT */ StateAndContract(state, PROGRAM_ID) ) SpreadOperator:DummyContract.kt$DummyContract.Companion$(*items) - SpreadOperator:DummyContractV2.kt$DummyContractV2.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owners.map { it.owningKey }), /* OUTPUT */ StateAndContract(state, DummyContractV2.PROGRAM_ID) ) SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*fields) SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*fields, cause.staticLocationBasedHash(hashedFields, visited + cause)) SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*hashedFields.invoke(this)) @@ -1491,7 +1476,6 @@ TooGenericExceptionCaught:BankOfCordaWebApi.kt$BankOfCordaWebApi$e: Exception TooGenericExceptionCaught:BlobInspector.kt$BlobInspector$e: Exception TooGenericExceptionCaught:BootstrapperView.kt$BootstrapperView$e: Exception - TooGenericExceptionCaught:BridgeControlListener.kt$BridgeControlListener$ex: Exception TooGenericExceptionCaught:BrokerJaasLoginModule.kt$BrokerJaasLoginModule$e: Exception TooGenericExceptionCaught:CertRole.kt$CertRole.Companion$ex: ArrayIndexOutOfBoundsException TooGenericExceptionCaught:CheckpointAgent.kt$CheckpointAgent.Companion$e: Exception @@ -1506,9 +1490,6 @@ TooGenericExceptionCaught:ContractUpgradeTransactions.kt$ContractUpgradeWireTransaction$e: Exception TooGenericExceptionCaught:CordaAuthenticationPlugin.kt$CordaAuthenticationPlugin$e: Exception TooGenericExceptionCaught:CordaClassResolver.kt$LoggingWhitelist.Companion$ioEx: Exception - TooGenericExceptionCaught:CordaFutureImpl.kt$CordaFutureImpl$e: Exception - TooGenericExceptionCaught:CordaFutureImpl.kt$ValueOrException$e: Exception - TooGenericExceptionCaught:CordaFutureImpl.kt$e: Exception TooGenericExceptionCaught:CordaPersistence.kt$CordaPersistence$e: Exception TooGenericExceptionCaught:CordaRPCClientTest.kt$CordaRPCClientTest$e: Exception TooGenericExceptionCaught:CordaRPCOpsImpl.kt$CordaRPCOpsImpl$e: Exception @@ -1644,7 +1625,6 @@ TooGenericExceptionCaught:ValidatingNotaryFlow.kt$ValidatingNotaryFlow$e: Exception TooGenericExceptionCaught:VaultStateMigration.kt$VaultStateIterator$e: Exception TooGenericExceptionCaught:VaultStateMigration.kt$VaultStateMigration$e: Exception - TooGenericExceptionCaught:VersionedParsingExampleTest.kt$VersionedParsingExampleTest.RpcSettingsSpec$e: Exception TooGenericExceptionCaught:WebServer.kt$WebServer$e: Exception TooGenericExceptionCaught:WebServer.kt$e: Exception TooGenericExceptionCaught:WebServer.kt$ex: Exception @@ -1699,7 +1679,6 @@ TooManyFunctions:CryptoUtils.kt$net.corda.core.crypto.CryptoUtils.kt TooManyFunctions:Currencies.kt$net.corda.finance.Currencies.kt TooManyFunctions:Driver.kt$DriverParameters - TooManyFunctions:DriverDSLImpl.kt$DriverDSLImpl : InternalDriverDSL TooManyFunctions:EncodingUtils.kt$net.corda.core.utilities.EncodingUtils.kt TooManyFunctions:FlowLogic.kt$FlowLogic<out T> TooManyFunctions:FlowStateMachineImpl.kt$FlowStateMachineImpl<R> : FiberFlowStateMachineFlowFiber @@ -1885,8 +1864,6 @@ VariableNaming:VaultQueryTests.kt$VaultQueryTestsBase$// Beware: do not use `MyContractClass::class.qualifiedName` as this returns a fully qualified name using "dot" notation for enclosed class val MYCONTRACT_ID = "net.corda.node.services.vault.VaultQueryTestsBase\$MyContractClass" VariableNaming:ZeroCouponBond.kt$ZeroCouponBond$val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z") WildcardImport:AMQPClient.kt$import io.netty.channel.* - WildcardImport:AMQPClientSerializationScheme.kt$import net.corda.serialization.internal.* - WildcardImport:AMQPClientSerializationScheme.kt$import net.corda.serialization.internal.amqp.* WildcardImport:AMQPRemoteTypeModel.kt$import net.corda.serialization.internal.model.* WildcardImport:AMQPSerializationScheme.kt$import net.corda.core.serialization.* WildcardImport:AMQPServerSerializationScheme.kt$import net.corda.serialization.internal.amqp.* @@ -2023,9 +2000,6 @@ WildcardImport:CordaModule.kt$import net.corda.core.crypto.* WildcardImport:CordaModule.kt$import net.corda.core.identity.* WildcardImport:CordaModule.kt$import net.corda.core.transactions.* - WildcardImport:CordaRPCClientTest.kt$import net.corda.core.context.* - WildcardImport:CordaRPCClientTest.kt$import net.corda.core.messaging.* - WildcardImport:CordaRPCClientTest.kt$import net.corda.testing.core.* WildcardImport:CordaRPCOps.kt$import net.corda.core.node.services.vault.* WildcardImport:CordaRPCOpsImplTest.kt$import net.corda.core.messaging.* WildcardImport:CordaRPCOpsImplTest.kt$import org.assertj.core.api.Assertions.* @@ -2121,8 +2095,6 @@ WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.flows.* WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.internal.* WildcardImport:FlowsDrainingModeContentionTest.kt$import net.corda.core.flows.* - WildcardImport:FxTransactionBuildTutorial.kt$import net.corda.core.contracts.* - WildcardImport:FxTransactionBuildTutorial.kt$import net.corda.core.flows.* WildcardImport:FxTransactionBuildTutorialTest.kt$import net.corda.finance.* WildcardImport:GenericsTests.kt$import net.corda.serialization.internal.amqp.testutils.* WildcardImport:Gui.kt$import tornadofx.* @@ -2168,10 +2140,7 @@ WildcardImport:InternalMockNetwork.kt$import net.corda.core.internal.* WildcardImport:InternalMockNetwork.kt$import net.corda.node.services.config.* WildcardImport:InternalMockNetwork.kt$import net.corda.testing.node.* - WildcardImport:InternalSerializationTestHelpers.kt$import net.corda.serialization.internal.* WildcardImport:InternalTestUtils.kt$import net.corda.core.contracts.* - WildcardImport:InternalUtils.kt$import java.security.cert.* - WildcardImport:InternalUtils.kt$import net.corda.core.crypto.* WildcardImport:IssuerModel.kt$import tornadofx.* WildcardImport:JVMConfig.kt$import tornadofx.* WildcardImport:JacksonSupport.kt$import com.fasterxml.jackson.core.* @@ -2249,7 +2218,6 @@ WildcardImport:NetworkBootstrapper.kt$import net.corda.nodeapi.internal.* WildcardImport:NetworkBootstrapperRunnerTests.kt$import org.junit.* WildcardImport:NetworkBootstrapperTest.kt$import net.corda.core.internal.* - WildcardImport:NetworkBootstrapperTest.kt$import net.corda.testing.core.* WildcardImport:NetworkBuilder.kt$import net.corda.networkbuilder.nodes.* WildcardImport:NetworkIdentityModel.kt$import net.corda.client.jfx.utils.* WildcardImport:NetworkMapServer.kt$import javax.ws.rs.* @@ -2362,7 +2330,6 @@ WildcardImport:PathUtils.kt$import java.io.* WildcardImport:PathUtils.kt$import java.nio.file.* WildcardImport:PersistentIdentityMigrationNewTableTest.kt$import net.corda.testing.core.* - WildcardImport:PersistentIdentityServiceTests.kt$import net.corda.testing.core.* WildcardImport:PersistentNetworkMapCacheTest.kt$import net.corda.testing.core.* WildcardImport:PersistentStateServiceTests.kt$import net.corda.core.contracts.* WildcardImport:Portfolio.kt$import net.corda.core.contracts.* @@ -2387,8 +2354,6 @@ WildcardImport:QueryCriteriaUtils.kt$import net.corda.core.node.services.vault.LikenessOperator.* WildcardImport:RPCMultipleInterfacesTests.kt$import org.junit.Assert.* WildcardImport:RPCSecurityManagerImpl.kt$import org.apache.shiro.authc.* - WildcardImport:RPCServer.kt$import net.corda.core.utilities.* - WildcardImport:RPCServer.kt$import org.apache.activemq.artemis.api.core.client.* WildcardImport:ReceiveFinalityFlowTest.kt$import net.corda.node.services.statemachine.StaffedFlowHospital.* WildcardImport:ReceiveFinalityFlowTest.kt$import net.corda.testing.node.internal.* WildcardImport:ReceiveTransactionFlow.kt$import net.corda.core.contracts.* @@ -2427,7 +2392,6 @@ WildcardImport:SearchField.kt$import tornadofx.* WildcardImport:SecureHashTest.kt$import org.junit.Assert.* WildcardImport:SendTransactionFlow.kt$import net.corda.core.internal.* - WildcardImport:SerializationEnvironmentRule.kt$import net.corda.testing.internal.* WildcardImport:SerializationHelper.kt$import java.lang.reflect.* WildcardImport:SerializationHelper.kt$import net.corda.core.serialization.* WildcardImport:SerializationOutputTests.kt$import java.time.* @@ -2572,7 +2536,6 @@ WildcardImport:VaultWithCashTest.kt$import net.corda.testing.core.* WildcardImport:VaultWithCashTest.kt$import net.corda.testing.internal.vault.* WildcardImport:VerifyTransactionTest.kt$import net.corda.finance.contracts.asset.Cash.Commands.* - WildcardImport:VersionedParsingExampleTest.kt$import net.corda.common.configuration.parsing.internal.* WildcardImport:WebServerController.kt$import tornadofx.* WildcardImport:WhitelistBasedTypeModelConfiguration.kt$import org.apache.qpid.proton.amqp.* WildcardImport:WhitelistGenerator.kt$import net.corda.core.internal.* @@ -2581,8 +2544,6 @@ WildcardImport:WireTransaction.kt$import net.corda.core.internal.* WildcardImport:WithFinality.kt$import net.corda.core.flows.* WildcardImport:WithMockNet.kt$import com.natpryce.hamkrest.* - WildcardImport:WorkflowTransactionBuildTutorial.kt$import net.corda.core.contracts.* - WildcardImport:WorkflowTransactionBuildTutorial.kt$import net.corda.core.flows.* WildcardImport:X509CRLSerializer.kt$import net.corda.serialization.internal.amqp.* WildcardImport:X509CertificateSerializer.kt$import net.corda.serialization.internal.amqp.* WildcardImport:X509EdDSAEngine.kt$import java.security.* diff --git a/node-api/build.gradle b/node-api/build.gradle index 2f5f774b9a..a8a8607cf6 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -19,6 +19,8 @@ dependencies { compile "org.apache.activemq:artemis-core-client:${artemis_version}" compile "org.apache.activemq:artemis-commons:${artemis_version}" + compile "io.netty:netty-handler-proxy:$netty_version" + // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" 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 b0957af4e4..1206cbe8ec 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 @@ -4,6 +4,9 @@ import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransportFromList +import net.corda.nodeapi.internal.config.MessagingServerConnectionConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -17,28 +20,55 @@ interface ArtemisSessionProvider { class ArtemisMessagingClient(private val config: MutualSslConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int, - private val failoverCallback: ((FailoverEventType) -> Unit)? = null) : ArtemisSessionProvider { + private val autoCommitSends: Boolean = true, + private val autoCommitAcks: Boolean = true, + private val confirmationWindowSize: Int = -1, + private val messagingServerConnectionConfig: MessagingServerConnectionConfiguration? = null, + private val backupServerAddressPool: List = emptyList(), + private val failoverCallback: ((FailoverEventType) -> Unit)? = null +) : ArtemisSessionProvider { companion object { private val log = loggerFor() + const val CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME = "net.corda.nodeapi.artemismessagingclient.CallTimeout" + const val CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT = 5000L } - class Started(val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer) + class Started(val serverLocator: ServerLocator, val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer) override var started: Started? = null private set override fun start(): Started = synchronized(this) { check(started == null) { "start can't be called twice" } + val tcpTransport = p2pConnectorTcpTransport(serverAddress, config) + val backupTransports = p2pConnectorTcpTransportFromList(backupServerAddressPool, config) + log.info("Connecting to message broker: $serverAddress") - // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config) - val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { + if (backupTransports.isNotEmpty()) { + log.info("Back-up message broker addresses: $backupServerAddressPool") + } + // If back-up artemis addresses are configured, the locator will be created using HA mode. + @Suppress("SpreadOperator") + val locator = ActiveMQClient.createServerLocator(backupTransports.isNotEmpty(), *(listOf(tcpTransport) + backupTransports).toTypedArray()).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 = 60000 clientFailureCheckPeriod = 30000 + callFailoverTimeout = java.lang.Long.getLong(CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME, CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT) + callTimeout = java.lang.Long.getLong(CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME, CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT) minLargeMessageSize = maxMessageSize isUseGlobalPools = nodeSerializationEnv != null + confirmationWindowSize = this@ArtemisMessagingClient.confirmationWindowSize + producerWindowSize = -1 + messagingServerConnectionConfig?.let { + connectionLoadBalancingPolicyClassName = RoundRobinConnectionPolicy::class.java.canonicalName + reconnectAttempts = messagingServerConnectionConfig.reconnectAttempts(isHA) + retryInterval = messagingServerConnectionConfig.retryInterval().toMillis() + retryIntervalMultiplier = messagingServerConnectionConfig.retryIntervalMultiplier() + maxRetryInterval = messagingServerConnectionConfig.maxRetryInterval(isHA).toMillis() + isFailoverOnInitialConnection = messagingServerConnectionConfig.failoverOnInitialAttempt(isHA) + initialConnectAttempts = messagingServerConnectionConfig.initialConnectAttempts(isHA) + } addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize)) } val sessionFactory = locator.createSessionFactory() @@ -50,23 +80,24 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration, // using our TLS certificate. // Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer // size of 1MB is acknowledged. - val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, autoCommitSends, autoCommitAcks, false, DEFAULT_ACK_BATCH_SIZE) session.start() // Create a general purpose producer. val producer = session.createProducer() - return Started(sessionFactory, session, producer).also { started = it } + return Started(locator, sessionFactory, session, producer).also { started = it } } override fun stop() = synchronized(this) { started?.run { producer.close() // Since we are leaking the session outside of this class it may well be already closed. - if(!session.isClosed) { + if (session.stillOpen()) { // Ensure any trailing messages are committed to the journal session.commit() } // Closing the factory closes all the sessions it produced as well. sessionFactory.close() + serverLocator.close() } started = null } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index 14052c1789..0da47bc2df 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -34,6 +34,7 @@ class ArtemisMessagingComponent { // This is a rough guess on the extra space needed on top of maxMessageSize to store the journal. // TODO: we might want to make this value configurable. const val JOURNAL_HEADER_SIZE = 1024 + object P2PMessagingHeaders { // This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic". // We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint @@ -123,6 +124,11 @@ class ArtemisMessagingComponent { require(address.startsWith(PEERS_PREFIX)) { "Failed to map address: $address to a remote topic as it is not in the $PEERS_PREFIX namespace" } return P2P_PREFIX + address.substring(PEERS_PREFIX.length) } + + fun translateInboxAddressToLocalQueue(address: String): String { + require(address.startsWith(P2P_PREFIX)) { "Failed to map topic: $address to a local address as it is not in the $P2P_PREFIX namespace" } + return PEERS_PREFIX + address.substring(P2P_PREFIX.length) + } } override val queueName: String = "$P2P_PREFIX${identity.toStringShort()}" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt index d1ae947fc3..d3122c9dc8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt @@ -100,35 +100,43 @@ class ArtemisTcpTransport { fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { - return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false) } - fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): TransportConfiguration { - return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false, keyStoreProvider = keyStoreProvider) } - fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { + fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() if (enableSSL) { options.putAll(defaultSSLOptions) (keyStore to trustStore).addToTransportOptions(options) + options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER } options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 // Suppress core.server.lambda$channelActive$0 - AMQ224088 error from load balancer type connections return TransportConfiguration(acceptorFactoryClassName, options) } - fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { + @Suppress("LongParameterList") + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false, keyStoreProvider: String? = null): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() if (enableSSL) { options.putAll(defaultSSLOptions) (keyStore to trustStore).addToTransportOptions(options) + options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER + keyStoreProvider?.let { options.put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, keyStoreProvider) } } return TransportConfiguration(connectorFactoryClassName, options) } + fun p2pConnectorTcpTransportFromList(hostAndPortList: List, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): List = hostAndPortList.map { + p2pConnectorTcpTransport(it, config, enableSSL, keyStoreProvider) + } + fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() @@ -156,12 +164,17 @@ class ArtemisTcpTransport { rpcConnectorTcpTransport(it, config, enableSSL) } - fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { - return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) + fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration { + return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions() + asMap(keyStoreProvider)) } - fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { - return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions() + (TransportConstants.HANDSHAKE_TIMEOUT to 0)) + fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration { + return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + + config.toTransportOptions() + (TransportConstants.HANDSHAKE_TIMEOUT to 0) + asMap(keyStoreProvider)) + } + + private fun asMap(keyStoreProvider: String?): Map { + return keyStoreProvider?.let {mutableMapOf(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to it)} ?: emptyMap() } } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisUtils.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisUtils.kt index 97abc0d024..23bb9d1428 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisUtils.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisUtils.kt @@ -1,5 +1,4 @@ @file:JvmName("ArtemisUtils") - package net.corda.nodeapi.internal import java.nio.file.FileSystems @@ -16,3 +15,4 @@ fun Path.requireOnDefaultFileSystem() { fun requireMessageSize(messageSize: Int, limit: Int) { require(messageSize <= limit) { "Message exceeds maxMessageSize network parameter, maxMessageSize: [$limit], message size: [$messageSize]" } } + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClientSessionUtils.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClientSessionUtils.kt new file mode 100644 index 0000000000..1b7ae2aea7 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClientSessionUtils.kt @@ -0,0 +1,8 @@ +package net.corda.nodeapi.internal + +import org.apache.activemq.artemis.api.core.client.ClientSession +import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal + +fun ClientSession.stillOpen(): Boolean { + return (!isClosed && (this as? ClientSessionInternal)?.isClosing != false) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ConcurrentBox.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ConcurrentBox.kt new file mode 100644 index 0000000000..1ae2807c4d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ConcurrentBox.kt @@ -0,0 +1,17 @@ +package net.corda.nodeapi.internal + +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * A [ConcurrentBox] allows the implementation of track() with reduced contention. [concurrent] may be run from several + * threads (which means it MUST be threadsafe!), while [exclusive] stops the world until the tracking has been set up. + * Internally [ConcurrentBox] is implemented simply as a read-write lock. + */ +class ConcurrentBox(val content: T) { + val lock = ReentrantReadWriteLock() + + inline fun concurrent(block: T.() -> R): R = lock.read { block(content) } + inline fun exclusive(block: T.() -> R): R = lock.write { block(content) } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/RoundRobinConnectionPolicy.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/RoundRobinConnectionPolicy.kt new file mode 100644 index 0000000000..9f2b0135db --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/RoundRobinConnectionPolicy.kt @@ -0,0 +1,18 @@ +package net.corda.nodeapi.internal + +import org.apache.activemq.artemis.api.core.client.loadbalance.ConnectionLoadBalancingPolicy + +/** + * Implementation of an Artemis load balancing policy. It does round-robin always starting from the first position, whereas + * the current [RoundRobinConnectionLoadBalancingPolicy] in Artemis picks the starting position randomly. This can lead to + * attempting to connect to an inactive broker on the first attempt, which can cause start-up delays depending on what connection + * settings are used. + */ +class RoundRobinConnectionPolicy : ConnectionLoadBalancingPolicy { + private var pos = 0 + + override fun select(max: Int): Int { + pos = if (pos >= max) 0 else pos + return pos++ + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index ed990acd03..40523033f2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -1,5 +1,8 @@ +@file:Suppress("TooGenericExceptionCaught") // needs to catch and handle/rethrow *all* exceptions in many places package net.corda.nodeapi.internal.bridging +import com.google.common.util.concurrent.ThreadFactoryBuilder +import io.netty.channel.EventLoop import io.netty.channel.EventLoopGroup import io.netty.channel.nio.NioEventLoopGroup import net.corda.core.identity.CordaX500Name @@ -11,11 +14,14 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_U import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.ArtemisSessionProvider +import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY import net.corda.nodeapi.internal.config.CertificateStore -import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration +import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig +import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig +import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ClientConsumer @@ -23,6 +29,10 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.api.core.client.ClientSession import org.slf4j.MDC import rx.Subscription +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -34,33 +44,46 @@ import kotlin.concurrent.withLock * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. */ @VisibleForTesting -class AMQPBridgeManager(config: MutualSslConfiguration, - maxMessageSize: Int, - crlCheckSoftFail: Boolean, - private val artemisMessageClientFactory: () -> ArtemisSessionProvider, - private val bridgeMetricsService: BridgeMetricsService? = null) : BridgeManager { +open class AMQPBridgeManager(keyStore: CertificateStore, + trustStore: CertificateStore, + useOpenSSL: Boolean, + proxyConfig: ProxyConfig? = null, + maxMessageSize: Int, + revocationConfig: RevocationConfig, + enableSNI: Boolean, + private val artemisMessageClientFactory: () -> ArtemisSessionProvider, + private val bridgeMetricsService: BridgeMetricsService? = null, + trace: Boolean, + sslHandshakeTimeout: Long?, + private val bridgeConnectionTTLSeconds: Int) : BridgeManager { private val lock = ReentrantLock() private val queueNamesToBridgesMap = mutableMapOf>() - private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore, - override val trustStore: CertificateStore, - override val maxMessageSize: Int, - override val crlCheckSoftFail: Boolean) : AMQPConfiguration { - constructor(config: MutualSslConfiguration, maxMessageSize: Int, crlCheckSoftFail: Boolean) : this(config.keyStore.get(), config.trustStore.get(), maxMessageSize, crlCheckSoftFail) + private class AMQPConfigurationImpl(override val keyStore: CertificateStore, + override val trustStore: CertificateStore, + override val proxyConfig: ProxyConfig?, + override val maxMessageSize: Int, + override val revocationConfig: RevocationConfig, + override val useOpenSsl: Boolean, + override val enableSNI: Boolean, + override val sourceX500Name: String? = null, + override val trace: Boolean, + private val _sslHandshakeTimeout: Long?) : AMQPConfiguration { + override val sslHandshakeTimeout: Long + get() = _sslHandshakeTimeout ?: super.sslHandshakeTimeout } - private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(config, maxMessageSize, crlCheckSoftFail) + private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize, revocationConfig,useOpenSSL, enableSNI, trace = trace, _sslHandshakeTimeout = sslHandshakeTimeout) private var sharedEventLoopGroup: EventLoopGroup? = null private var artemis: ArtemisSessionProvider? = null - constructor(config: MutualSslConfiguration, - p2pAddress: NetworkHostAndPort, - maxMessageSize: Int, - crlCheckSoftFail: Boolean) : this(config, maxMessageSize, crlCheckSoftFail, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) - companion object { - private const val NUM_BRIDGE_THREADS = 0 // Default sized pool + + private const val CORDA_NUM_BRIDGE_THREADS_PROP_NAME = "net.corda.nodeapi.amqpbridgemanager.NumBridgeThreads" + + private val NUM_BRIDGE_THREADS = Integer.getInteger(CORDA_NUM_BRIDGE_THREADS_PROP_NAME, 0) // Default 0 means Netty default sized pool + private const val ARTEMIS_RETRY_BACKOFF = 5000L } /** @@ -71,13 +94,16 @@ class AMQPBridgeManager(config: MutualSslConfiguration, * If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery, * however Artemis and the remote Corda instanced will deduplicate these messages. */ - private class AMQPBridge(val queueName: String, + @Suppress("TooManyFunctions") + private class AMQPBridge(val sourceX500Name: String, + val queueName: String, val targets: List, val legalNames: Set, private val amqpConfig: AMQPConfiguration, sharedEventGroup: EventLoopGroup, private val artemis: ArtemisSessionProvider, - private val bridgeMetricsService: BridgeMetricsService?) { + private val bridgeMetricsService: BridgeMetricsService?, + private val bridgeConnectionTTLSeconds: Int) { companion object { private val log = contextLogger() } @@ -86,6 +112,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, val oldMDC = MDC.getCopyOfContextMap() ?: emptyMap() try { MDC.put("queueName", queueName) + MDC.put("source", amqpConfig.sourceX500Name) MDC.put("targets", targets.joinToString(separator = ";") { it.toString() }) MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() }) MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString()) @@ -106,10 +133,80 @@ class AMQPBridgeManager(config: MutualSslConfiguration, private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) } val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup) - private val lock = ReentrantLock() // lock to serialise session level access private var session: ClientSession? = null private var consumer: ClientConsumer? = null private var connectedSubscription: Subscription? = null + @Volatile + private var messagesReceived: Boolean = false + private val eventLoop: EventLoop = sharedEventGroup.next() + private var artemisState: ArtemisState = ArtemisState.STOPPED + set(value) { + logDebugWithMDC { "State change $field to $value" } + field = value + } + @Suppress("MagicNumber") + private var artemisHeartbeatPlusBackoff = TimeUnit.SECONDS.toMillis(90) + private var amqpRestartEvent: ScheduledFuture? = null + private var scheduledExecutorService: ScheduledExecutorService + = Executors.newSingleThreadScheduledExecutor(ThreadFactoryBuilder().setNameFormat("bridge-connection-reset-%d").build()) + + @Suppress("ClassNaming") + private sealed class ArtemisState { + object STARTING : ArtemisState() + data class STARTED(override val pending: ScheduledFuture) : ArtemisState() + + object CHECKING : ArtemisState() + object RESTARTED : ArtemisState() + object RECEIVING : ArtemisState() + + object AMQP_STOPPED : ArtemisState() + object AMQP_STARTING : ArtemisState() + object AMQP_STARTED : ArtemisState() + object AMQP_RESTARTED : ArtemisState() + + object STOPPING : ArtemisState() + object STOPPED : ArtemisState() + data class STOPPED_AMQP_START_SCHEDULED(override val pending: ScheduledFuture) : ArtemisState() + + open val pending: ScheduledFuture? = null + + override fun toString(): String = javaClass.simpleName + } + + private fun artemis(inProgress: ArtemisState, block: (precedingState: ArtemisState) -> ArtemisState) { + val runnable = { + synchronized(artemis) { + try { + val precedingState = artemisState + artemisState.pending?.cancel(false) + artemisState = inProgress + artemisState = block(precedingState) + } catch (ex: Exception) { + withMDC { log.error("Unexpected error in Artemis processing in state $artemisState.", ex) } + } + } + } + if (eventLoop.inEventLoop()) { + runnable() + } else { + eventLoop.execute(runnable) + } + } + + private fun scheduledArtemis(delay: Long, unit: TimeUnit, inProgress: ArtemisState, block: (precedingState: ArtemisState) -> ArtemisState): ScheduledFuture { + return eventLoop.schedule({ + artemis(inProgress, block) + }, delay, unit) + } + + private fun scheduledArtemisInExecutor(delay: Long, unit: TimeUnit, inProgress: ArtemisState, nextState: ArtemisState, block: () -> Unit): ScheduledFuture { + return scheduledExecutorService.schedule({ + artemis(inProgress) { + nextState + } + block() + }, delay, unit) + } fun start() { logInfoWithMDC("Create new AMQP bridge") @@ -119,55 +216,196 @@ class AMQPBridgeManager(config: MutualSslConfiguration, fun stop() { logInfoWithMDC("Stopping AMQP bridge") - lock.withLock { - synchronized(artemis) { - consumer?.close() - consumer = null - session?.stop() - session = null - } - } - amqpClient.stop() - connectedSubscription?.unsubscribe() - connectedSubscription = null - } - - private fun onSocketConnected(connected: Boolean) { - lock.withLock { - synchronized(artemis) { - if (connected) { - logInfoWithMDC("Bridge Connected") - bridgeMetricsService?.bridgeConnected(targets, legalNames) - val sessionFactory = artemis.started!!.sessionFactory - val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - this.session = session - val consumer = session.createConsumer(queueName) - this.consumer = consumer - consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler) - session.start() - } else { - logInfoWithMDC("Bridge Disconnected") - bridgeMetricsService?.bridgeDisconnected(targets, legalNames) - consumer?.close() - consumer = null - session?.stop() + artemis(ArtemisState.STOPPING) { + logInfoWithMDC("Stopping Artemis because stopping AMQP bridge") + closeConsumer() + consumer = null + eventLoop.execute { + artemis(ArtemisState.STOPPING) { + stopSession() session = null + ArtemisState.STOPPED } } + ArtemisState.STOPPING } + bridgeMetricsService?.bridgeDisconnected(targets, legalNames) + connectedSubscription?.unsubscribe() + connectedSubscription = null + // Do this last because we already scheduled the Artemis stop, so it's okay to unsubscribe onConnected first. + amqpClient.stop() + } + + @Suppress("ComplexMethod") + private fun onSocketConnected(connected: Boolean) { + if (connected) { + logInfoWithMDC("Bridge Connected") + + bridgeMetricsService?.bridgeConnected(targets, legalNames) + if (bridgeConnectionTTLSeconds > 0) { + // AMQP outbound connection will be restarted periodically with bridgeConnectionTTLSeconds interval + amqpRestartEvent = scheduledArtemisInExecutor(bridgeConnectionTTLSeconds.toLong(), TimeUnit.SECONDS, + ArtemisState.AMQP_STOPPED, ArtemisState.AMQP_RESTARTED) { + logInfoWithMDC("Bridge connection time to live exceeded. Restarting AMQP connection") + stopAndStartOutbound(ArtemisState.AMQP_RESTARTED) + } + } + artemis(ArtemisState.STARTING) { + val startedArtemis = artemis.started + if (startedArtemis == null) { + logInfoWithMDC("Bridge Connected but Artemis is disconnected") + ArtemisState.STOPPED + } else { + logInfoWithMDC("Bridge Connected so starting Artemis") + artemisHeartbeatPlusBackoff = startedArtemis.serverLocator.connectionTTL + ARTEMIS_RETRY_BACKOFF + try { + createSessionAndConsumer(startedArtemis) + ArtemisState.STARTED(scheduledArtemis(artemisHeartbeatPlusBackoff, TimeUnit.MILLISECONDS, ArtemisState.CHECKING) { + if (!messagesReceived) { + logInfoWithMDC("No messages received on new bridge. Restarting Artemis session") + if (restartSession()) { + ArtemisState.RESTARTED + } else { + logInfoWithMDC("Artemis session restart failed. Aborting by restarting AMQP connection.") + stopAndStartOutbound() + } + } else { + ArtemisState.RECEIVING + } + }) + } catch (ex: Exception) { + // Now, bounce the AMQP connection to restart the sequence of establishing the connectivity back from the beginning. + withMDC { log.warn("Create Artemis start session error. Restarting AMQP connection", ex) } + stopAndStartOutbound() + } + } + } + } else { + logInfoWithMDC("Bridge Disconnected") + amqpRestartEvent?.cancel(false) + if (artemisState != ArtemisState.AMQP_STARTING && artemisState != ArtemisState.STOPPED) { + bridgeMetricsService?.bridgeDisconnected(targets, legalNames) + } + artemis(ArtemisState.STOPPING) { precedingState: ArtemisState -> + logInfoWithMDC("Stopping Artemis because AMQP bridge disconnected") + closeConsumer() + consumer = null + eventLoop.execute { + artemis(ArtemisState.STOPPING) { + stopSession() + session = null + when (precedingState) { + ArtemisState.AMQP_STOPPED -> + ArtemisState.STOPPED_AMQP_START_SCHEDULED(scheduledArtemis(artemisHeartbeatPlusBackoff, + TimeUnit.MILLISECONDS, ArtemisState.AMQP_STARTING) { startOutbound() }) + ArtemisState.AMQP_RESTARTED -> { + artemis(ArtemisState.AMQP_STARTING) { startOutbound() } + ArtemisState.AMQP_STARTING + } + else -> ArtemisState.STOPPED + } + } + } + ArtemisState.STOPPING + } + } + } + + private fun startOutbound(): ArtemisState { + logInfoWithMDC("Starting AMQP client") + amqpClient.start() + return ArtemisState.AMQP_STARTED + } + + private fun stopAndStartOutbound(state: ArtemisState = ArtemisState.AMQP_STOPPED): ArtemisState { + amqpClient.stop() + // Bridge disconnect will detect this state and schedule an AMQP start. + return state + } + + private fun createSessionAndConsumer(startedArtemis: ArtemisMessagingClient.Started): ClientSession { + logInfoWithMDC("Creating session and consumer.") + val sessionFactory = startedArtemis.sessionFactory + val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, + true, false, DEFAULT_ACK_BATCH_SIZE) + this.session = session + // Several producers (in the case of shared bridge) can put messages in the same outbound p2p queue. + // The consumers are created using the source x500 name as a filter + val consumer = if (amqpConfig.enableSNI) { + session.createConsumer(queueName, "hyphenated_props:sender-subject-name = '${amqpConfig.sourceX500Name}'") + } else { + session.createConsumer(queueName) + } + this.consumer = consumer + session.start() + consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler) + return session + } + + private fun closeConsumer(): Boolean { + var closed = false + try { + consumer?.apply { + if (!isClosed) { + close() + } + } + closed = true + } catch (ex: Exception) { + withMDC { log.warn("Close artemis consumer error", ex) } + } finally { + return closed + } + } + + private fun stopSession(): Boolean { + var stopped = false + try { + session?.apply { + if (!isClosed) { + stop() + } + } + stopped = true + } catch (ex: Exception) { + withMDC { log.warn("Stop Artemis session error", ex) } + } finally { + return stopped + } + } + + private fun restartSession(): Boolean { + if (!stopSession()) { + // Session timed out stopping. The request/responses can be out of sequence on the session now, so abandon it. + session = null + // The consumer is also dead now too as attached to the dead session. + consumer = null + return false + } + try { + // Does not wait for a response. + this.session?.start() + } catch (ex: Exception) { + withMDC { log.error("Start Artemis session error", ex) } + } + return true } private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { + messagesReceived = true if (artemisMessage.bodySize > amqpConfig.maxMessageSize) { val msg = "Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " + - "dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" + "dropping message, uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" logWarnWithMDC(msg) bridgeMetricsService?.packetDropEvent(artemisMessage, msg) // Ack the message to prevent same message being sent to us again. - artemisMessage.individualAcknowledge() + try { + artemisMessage.individualAcknowledge() + } catch (ex: ActiveMQObjectClosedException) { + log.warn("Artemis message was closed") + } return } - val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) } val properties = HashMap() for (key in P2PMessagingHeaders.whitelistedHeaders) { if (artemisMessage.containsProperty(key)) { @@ -178,18 +416,22 @@ class AMQPBridgeManager(config: MutualSslConfiguration, properties[key] = value } } - logDebugWithMDC { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" } + logDebugWithMDC { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" } val peerInbox = translateLocalQueueToInboxAddress(queueName) - val sendableMessage = amqpClient.createMessage(data, peerInbox, + val sendableMessage = amqpClient.createMessage(artemisMessage.payload(), peerInbox, legalNames.first().toString(), properties) sendableMessage.onComplete.then { logDebugWithMDC { "Bridge ACK ${sendableMessage.onComplete.get()}" } - lock.withLock { + eventLoop.submit { if (sendableMessage.onComplete.get() == MessageStatus.Acknowledged) { - artemisMessage.individualAcknowledge() + try { + artemisMessage.individualAcknowledge() + } catch (ex: ActiveMQObjectClosedException) { + log.warn("Artemis message was closed") + } } else { - logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}") + logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}") // We need to commit any acknowledged messages before rolling back the failed // (unacknowledged) message. session?.commit() @@ -202,9 +444,9 @@ class AMQPBridgeManager(config: MutualSslConfiguration, } catch (ex: IllegalStateException) { // Attempting to send a message while the AMQP client is disconnected may cause message loss. // The failed message is rolled back after committing acknowledged messages. - lock.withLock { - ex.message?.let { logInfoWithMDC(it)} - logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}") + eventLoop.submit { + ex.message?.let { logInfoWithMDC(it) } + logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}") session?.commit() session?.rollback(false) } @@ -213,20 +455,22 @@ class AMQPBridgeManager(config: MutualSslConfiguration, } } - override fun deployBridge(queueName: String, targets: List, legalNames: Set) { - val newBridge = lock.withLock { + override fun deployBridge(sourceX500Name: String, queueName: String, targets: List, legalNames: Set) { + lock.withLock { val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() } for (target in targets) { - if (bridges.any { it.targets.contains(target) }) { + if (bridges.any { it.targets.contains(target) && it.sourceX500Name == sourceX500Name }) { return } } - val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService) + val newAMQPConfig = with(amqpConfig) { AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize, + revocationConfig, useOpenSsl, enableSNI, sourceX500Name, trace, sslHandshakeTimeout) } + val newBridge = AMQPBridge(sourceX500Name, queueName, targets, legalNames, newAMQPConfig, sharedEventLoopGroup!!, artemis!!, + bridgeMetricsService, bridgeConnectionTTLSeconds) bridges += newBridge bridgeMetricsService?.bridgeCreated(targets, legalNames) newBridge - } - newBridge.start() + }.start() } override fun destroyBridge(queueName: String, targets: List) { @@ -246,6 +490,17 @@ class AMQPBridgeManager(config: MutualSslConfiguration, } } + fun destroyAllBridges(queueName: String): Map { + return lock.withLock { + // queueNamesToBridgesMap returns a mutable list, .toList converts it to a immutable list so it won't be changed by the [destroyBridge] method. + val bridges = queueNamesToBridgesMap[queueName]?.toList() + destroyBridge(queueName, bridges?.flatMap { it.targets } ?: emptyList()) + bridges?.map { + it.sourceX500Name to BridgeEntry(it.queueName, it.targets, it.legalNames.toList(), serviceAddress = false) + }?.toMap() ?: emptyMap() + } + } + override fun start() { sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) val artemis = artemisMessageClientFactory() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index c3c829b94a..2a37649667 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -1,51 +1,124 @@ +@file:Suppress("TooGenericExceptionCaught") // needs to catch and handle/rethrow *all* exceptions package net.corda.nodeapi.internal.bridging +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.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisSessionProvider +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.crypto.x509 +import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig +import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig +import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException +import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ClientConsumer import org.apache.activemq.artemis.api.core.client.ClientMessage +import org.apache.activemq.artemis.api.core.client.ClientSession +import rx.Observable +import rx.subjects.PublishSubject import java.util.* -class BridgeControlListener(val config: MutualSslConfiguration, +class BridgeControlListener(private val keyStore: CertificateStore, + trustStore: CertificateStore, + useOpenSSL: Boolean, + proxyConfig: ProxyConfig? = null, maxMessageSize: Int, - crlCheckSoftFail: Boolean, + revocationConfig: RevocationConfig, + enableSNI: Boolean, private val artemisMessageClientFactory: () -> ArtemisSessionProvider, - bridgeMetricsService: BridgeMetricsService? = null) : AutoCloseable { + bridgeMetricsService: BridgeMetricsService? = null, + trace: Boolean = false, + sslHandshakeTimeout: Long? = null, + bridgeConnectionTTLSeconds: Int = 0) : AutoCloseable { private val bridgeId: String = UUID.randomUUID().toString() - private val bridgeManager: BridgeManager = AMQPBridgeManager( - config, - maxMessageSize, - crlCheckSoftFail, - artemisMessageClientFactory, - bridgeMetricsService) + private var bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId" + private var bridgeNotifyQueue = "$BRIDGE_NOTIFY.$bridgeId" private val validInboundQueues = mutableSetOf() + private val bridgeManager = if (enableSNI) { + LoopbackBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig, maxMessageSize, revocationConfig, enableSNI, + artemisMessageClientFactory, bridgeMetricsService, this::validateReceiveTopic, trace, sslHandshakeTimeout, + bridgeConnectionTTLSeconds) + } else { + AMQPBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig, maxMessageSize, revocationConfig, enableSNI, + artemisMessageClientFactory, bridgeMetricsService, trace, sslHandshakeTimeout, bridgeConnectionTTLSeconds) + } private var artemis: ArtemisSessionProvider? = null private var controlConsumer: ClientConsumer? = null + private var notifyConsumer: ClientConsumer? = null + + constructor(config: MutualSslConfiguration, + p2pAddress: NetworkHostAndPort, + maxMessageSize: Int, + revocationConfig: RevocationConfig, + enableSNI: Boolean, + proxy: ProxyConfig? = null) : this(config.keyStore.get(), config.trustStore.get(), config.useOpenSsl, proxy, maxMessageSize, revocationConfig, enableSNI, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) companion object { private val log = contextLogger() } + val active: Boolean + get() = validInboundQueues.isNotEmpty() + + private val _activeChange = PublishSubject.create().toSerialized() + val activeChange: Observable + get() = _activeChange + + private val _failure = PublishSubject.create().toSerialized() + val failure: Observable + get() = _failure + fun start() { - stop() - bridgeManager.start() - val artemis = artemisMessageClientFactory() - this.artemis = artemis - artemis.start() - val artemisClient = artemis.started!! - val artemisSession = artemisClient.session - val bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId" - artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue) + try { + stop() + + val queueDisambiguityId = UUID.randomUUID().toString() + bridgeControlQueue = "$BRIDGE_CONTROL.$queueDisambiguityId" + bridgeNotifyQueue = "$BRIDGE_NOTIFY.$queueDisambiguityId" + + bridgeManager.start() + val artemis = artemisMessageClientFactory() + this.artemis = artemis + artemis.start() + val artemisClient = artemis.started!! + val artemisSession = artemisClient.session + registerBridgeControlListener(artemisSession) + registerBridgeDuplicateChecker(artemisSession) + // Attempt to read available inboxes directly from Artemis before requesting updates from connected nodes + validInboundQueues.addAll(artemisSession.addressQuery(SimpleString("$P2P_PREFIX#")).queueNames.map { it.toString() }) + log.info("Found inboxes: $validInboundQueues") + if (active) { + _activeChange.onNext(true) + } + val startupMessage = BridgeControl.BridgeToNodeSnapshotRequest(bridgeId).serialize(context = SerializationDefaults.P2P_CONTEXT) + .bytes + val bridgeRequest = artemisSession.createMessage(false) + bridgeRequest.writeBodyBufferBytes(startupMessage) + artemisClient.producer.send(BRIDGE_NOTIFY, bridgeRequest) + } catch (e: Exception) { + log.error("Failure to start BridgeControlListener", e) + _failure.onNext(this) + } + } + + private fun registerBridgeControlListener(artemisSession: ClientSession) { + try { + artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue) + } catch (ex: ActiveMQQueueExistsException) { + // Ignore if there is a queue still not cleaned up + } + val control = artemisSession.createConsumer(bridgeControlQueue) controlConsumer = control control.setMessageHandler { msg -> @@ -53,22 +126,64 @@ class BridgeControlListener(val config: MutualSslConfiguration, processControlMessage(msg) } catch (ex: Exception) { log.error("Unable to process bridge control message", ex) + _failure.onNext(this) + } + msg.acknowledge() + } + } + + private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) { + try { + artemisSession.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue) + } catch (ex: ActiveMQQueueExistsException) { + // Ignore if there is a queue still not cleaned up + } + val notify = artemisSession.createConsumer(bridgeNotifyQueue) + notifyConsumer = notify + notify.setMessageHandler { msg -> + try { + val data: ByteArray = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) } + val notifyMessage = data.deserialize(context = SerializationDefaults.P2P_CONTEXT) + if (notifyMessage.bridgeIdentity != bridgeId) { + log.error("Fatal Error! Two bridges have been configured simultaneously! Check the enterpriseConfiguration.externalBridge status") + System.exit(1) + } + } catch (ex: Exception) { + log.error("Unable to process bridge notification message", ex) + _failure.onNext(this) } msg.acknowledge() } - val startupMessage = BridgeControl.BridgeToNodeSnapshotRequest(bridgeId).serialize(context = SerializationDefaults.P2P_CONTEXT).bytes - val bridgeRequest = artemisSession.createMessage(false) - bridgeRequest.writeBodyBufferBytes(startupMessage) - artemisClient.producer.send(BRIDGE_NOTIFY, bridgeRequest) } fun stop() { - validInboundQueues.clear() - controlConsumer?.close() - controlConsumer = null - artemis?.stop() - artemis = null - bridgeManager.stop() + try { + if (active) { + _activeChange.onNext(false) + } + validInboundQueues.clear() + controlConsumer?.close() + controlConsumer = null + notifyConsumer?.close() + notifyConsumer = null + artemis?.apply { + try { + started?.session?.deleteQueue(bridgeControlQueue) + } catch (e: ActiveMQNonExistentQueueException) { + log.warn("Queue $bridgeControlQueue does not exist and it can't be deleted") + } + try { + started?.session?.deleteQueue(bridgeNotifyQueue) + } catch (e: ActiveMQNonExistentQueueException) { + log.warn("Queue $bridgeNotifyQueue does not exist and it can't be deleted") + } + stop() + } + artemis = null + bridgeManager.stop() + } catch (e: Exception) { + log.error("Failure to stop BridgeControlListener", e) + } } override fun close() = stop() @@ -91,6 +206,10 @@ class BridgeControlListener(val config: MutualSslConfiguration, log.info("Received bridge control message $controlMessage") when (controlMessage) { is BridgeControl.NodeToBridgeSnapshot -> { + if (!isConfigured(controlMessage.nodeIdentity)) { + log.error("Fatal error! Bridge not configured with keystore for node with legal name ${controlMessage.nodeIdentity}.") + System.exit(1) + } if (!controlMessage.inboxQueues.all { validateInboxQueueName(it) }) { log.error("Invalid queue names in control message $controlMessage") return @@ -99,10 +218,20 @@ class BridgeControlListener(val config: MutualSslConfiguration, log.error("Invalid queue names in control message $controlMessage") return } - for (outQueue in controlMessage.sendQueues) { - bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet()) - } + + val wasActive = active validInboundQueues.addAll(controlMessage.inboxQueues) + for (outQueue in controlMessage.sendQueues) { + bridgeManager.deployBridge(controlMessage.nodeIdentity, outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet()) + } + log.info("Added inbox: ${controlMessage.inboxQueues}. Current inboxes: $validInboundQueues.") + if (bridgeManager is LoopbackBridgeManager) { + // Notify loopback bridge manager inboxes has changed. + bridgeManager.inboxesAdded(controlMessage.inboxQueues) + } + if (!wasActive && active) { + _activeChange.onNext(true) + } } is BridgeControl.BridgeToNodeSnapshotRequest -> { log.error("Message from Bridge $controlMessage detected on wrong topic!") @@ -112,7 +241,7 @@ class BridgeControlListener(val config: MutualSslConfiguration, log.error("Invalid queue names in control message $controlMessage") return } - bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet()) + bridgeManager.deployBridge(controlMessage.nodeIdentity, controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet()) } is BridgeControl.Delete -> { if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) { @@ -121,7 +250,19 @@ class BridgeControlListener(val config: MutualSslConfiguration, } bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets) } + is BridgeControl.BridgeHealthCheck -> { + log.warn("Not currently doing anything on BridgeHealthCheck") + return + } } } + private fun isConfigured(sourceX500Name: String): Boolean { + val keyStore = keyStore.value.internal + return keyStore.aliases().toList().any { alias -> + val x500Name = keyStore.getCertificate(alias).x509.subjectX500Principal + val cordaX500Name = CordaX500Name.build(x500Name) + cordaX500Name.toString() == sourceX500Name + } + } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlMessages.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlMessages.kt index 6a2f30bcd4..7c820e4770 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlMessages.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlMessages.kt @@ -11,7 +11,7 @@ import net.corda.core.utilities.NetworkHostAndPort * @property legalNames The list of acceptable [CordaX500Name] names that should be presented as subject of the validated peer TLS certificate. */ @CordaSerializable -data class BridgeEntry(val queueName: String, val targets: List, val legalNames: List) +data class BridgeEntry(val queueName: String, val targets: List, val legalNames: List, val serviceAddress: Boolean) sealed class BridgeControl { /** @@ -47,4 +47,13 @@ sealed class BridgeControl { */ @CordaSerializable data class Delete(val nodeIdentity: String, val bridgeInfo: BridgeEntry) : BridgeControl() + + /** + * This message is sent to Bridge to check the health of it. + * @property requestId The identifier for the health check request as health check is likely to be produced repeatedly. + * @property command Allows to specify the sort fo health check that needs to be performed. + * @property bridgeInfo The connection details of the new bridge (optional). + */ + @CordaSerializable + data class BridgeHealthCheck(val requestId: Long, val command: String, val bridgeInfo: BridgeEntry?) : BridgeControl() } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt index 69b1509550..7bf1f150da 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt @@ -3,17 +3,20 @@ package net.corda.nodeapi.internal.bridging import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting import net.corda.core.utilities.NetworkHostAndPort +import org.apache.activemq.artemis.api.core.client.ClientMessage /** * Provides an internal interface that the [BridgeControlListener] delegates to for Bridge activities. */ @VisibleForTesting interface BridgeManager : AutoCloseable { - fun deployBridge(queueName: String, targets: List, legalNames: Set) + fun deployBridge(sourceX500Name: String, queueName: String, targets: List, legalNames: Set) fun destroyBridge(queueName: String, targets: List) fun start() fun stop() -} \ No newline at end of file +} + +fun ClientMessage.payload() = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt new file mode 100644 index 0000000000..fc27029584 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt @@ -0,0 +1,223 @@ +package net.corda.nodeapi.internal.bridging + +import net.corda.nodeapi.internal.ConcurrentBox +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER +import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateInboxAddressToLocalQueue +import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress +import net.corda.nodeapi.internal.ArtemisSessionProvider +import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl +import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig +import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig +import net.corda.nodeapi.internal.stillOpen +import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE +import org.apache.activemq.artemis.api.core.client.ClientConsumer +import org.apache.activemq.artemis.api.core.client.ClientMessage +import org.apache.activemq.artemis.api.core.client.ClientProducer +import org.apache.activemq.artemis.api.core.client.ClientSession +import org.slf4j.MDC + +/** + * The LoopbackBridgeManager holds the list of independent LoopbackBridge objects that actively loopback messages to local Artemis + * inboxes. + */ +@VisibleForTesting +class LoopbackBridgeManager(keyStore: CertificateStore, + trustStore: CertificateStore, + useOpenSSL: Boolean, + proxyConfig: ProxyConfig? = null, + maxMessageSize: Int, + revocationConfig: RevocationConfig, + enableSNI: Boolean, + private val artemisMessageClientFactory: () -> ArtemisSessionProvider, + private val bridgeMetricsService: BridgeMetricsService? = null, + private val isLocalInbox: (String) -> Boolean, + trace: Boolean, + sslHandshakeTimeout: Long? = null, + bridgeConnectionTTLSeconds: Int = 0) : AMQPBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig, + maxMessageSize, revocationConfig, enableSNI, + artemisMessageClientFactory, bridgeMetricsService, + trace, sslHandshakeTimeout, + bridgeConnectionTTLSeconds) { + + companion object { + private val log = contextLogger() + } + + private val queueNamesToBridgesMap = ConcurrentBox(mutableMapOf>()) + private var artemis: ArtemisSessionProvider? = null + + /** + * Each LoopbackBridge is an independent consumer of messages from the Artemis local queue per designated endpoint. + * It attempts to loopback these messages via ArtemisClient to the local inbox. + */ + private class LoopbackBridge(val sourceX500Name: String, + val queueName: String, + val targets: List, + val legalNames: Set, + artemis: ArtemisSessionProvider, + private val bridgeMetricsService: BridgeMetricsService?) { + companion object { + private val log = contextLogger() + } + + // TODO: refactor MDC support, duplicated in AMQPBridgeManager. + private fun withMDC(block: () -> Unit) { + val oldMDC = MDC.getCopyOfContextMap() + try { + MDC.put("queueName", queueName) + MDC.put("source", sourceX500Name) + MDC.put("targets", targets.joinToString(separator = ";") { it.toString() }) + MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() }) + MDC.put("bridgeType", "loopback") + block() + } finally { + MDC.setContextMap(oldMDC) + } + } + + private fun logDebugWithMDC(msg: () -> String) { + if (log.isDebugEnabled) { + withMDC { log.debug(msg()) } + } + } + + private fun logInfoWithMDC(msg: String) = withMDC { log.info(msg) } + + private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) } + + private val artemis = ConcurrentBox(artemis) + private var consumerSession: ClientSession? = null + private var producerSession: ClientSession? = null + private var consumer: ClientConsumer? = null + private var producer: ClientProducer? = null + + fun start() { + logInfoWithMDC("Create new Artemis loopback bridge") + artemis.exclusive { + logInfoWithMDC("Bridge Connected") + bridgeMetricsService?.bridgeConnected(targets, legalNames) + val sessionFactory = started!!.sessionFactory + this@LoopbackBridge.consumerSession = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + this@LoopbackBridge.producerSession = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + // Several producers (in the case of shared bridge) can put messages in the same outbound p2p queue. The consumers are created using the source x500 name as a filter + val consumer = consumerSession!!.createConsumer(queueName, "hyphenated_props:sender-subject-name = '$sourceX500Name'") + consumer.setMessageHandler(this@LoopbackBridge::clientArtemisMessageHandler) + this@LoopbackBridge.consumer = consumer + this@LoopbackBridge.producer = producerSession!!.createProducer() + consumerSession?.start() + producerSession?.start() + } + } + + fun stop() { + logInfoWithMDC("Stopping AMQP bridge") + artemis.exclusive { + bridgeMetricsService?.bridgeDisconnected(targets, legalNames) + consumer?.apply { if (!isClosed) close() } + consumer = null + producer?.apply { if (!isClosed) close() } + producer = null + consumerSession?.apply { if (stillOpen()) stop() } + consumerSession = null + producerSession?.apply { if (stillOpen()) stop()} + producerSession = null + } + } + + private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { + logDebugWithMDC { "Loopback Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" } + val peerInbox = translateLocalQueueToInboxAddress(queueName) + producer?.send(SimpleString(peerInbox), artemisMessage) { artemisMessage.individualAcknowledge() } + bridgeMetricsService?.let { metricsService -> + val properties = ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.mapNotNull { key -> + if (artemisMessage.containsProperty(key)) { + key to artemisMessage.getObjectProperty(key).let { (it as? SimpleString)?.toString() ?: it } + } else { + null + } + }.toMap() + metricsService.packetAcceptedEvent(SendableMessageImpl(artemisMessage.payload(), peerInbox, legalNames.first().toString(), targets.first(), properties)) + } + } + } + + override fun deployBridge(sourceX500Name: String, queueName: String, targets: List, legalNames: Set) { + val inboxAddress = translateLocalQueueToInboxAddress(queueName) + if (isLocalInbox(inboxAddress)) { + log.info("Deploying loopback bridge for $queueName, source $sourceX500Name") + queueNamesToBridgesMap.exclusive { + val bridges = getOrPut(queueName) { mutableListOf() } + for (target in targets) { + if (bridges.any { it.targets.contains(target) && it.sourceX500Name == sourceX500Name }) { + return + } + } + val newBridge = LoopbackBridge(sourceX500Name, queueName, targets, legalNames, artemis!!, bridgeMetricsService) + bridges += newBridge + bridgeMetricsService?.bridgeCreated(targets, legalNames) + newBridge + }.start() + } else { + log.info("Deploying AMQP bridge for $queueName, source $sourceX500Name") + super.deployBridge(sourceX500Name, queueName, targets, legalNames) + } + } + + override fun destroyBridge(queueName: String, targets: List) { + super.destroyBridge(queueName, targets) + queueNamesToBridgesMap.exclusive { + val bridges = this[queueName] ?: mutableListOf() + for (target in targets) { + val bridge = bridges.firstOrNull { it.targets.contains(target) } + if (bridge != null) { + bridges -= bridge + if (bridges.isEmpty()) { + remove(queueName) + } + bridge.stop() + bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames) + } + } + } + } + + /** + * Remove any AMQP bridge for the local inbox and create a loopback bridge for that queue. + */ + fun inboxesAdded(inboxes: List) { + for (inbox in inboxes) { + super.destroyAllBridges(translateInboxAddressToLocalQueue(inbox)).forEach { source, bridgeEntry -> + log.info("Destroyed AMQP Bridge '${bridgeEntry.queueName}', creating Loopback bridge for local inbox.") + deployBridge(source, bridgeEntry.queueName, bridgeEntry.targets, bridgeEntry.legalNames.toSet()) + } + } + } + + override fun start() { + super.start() + val artemis = artemisMessageClientFactory() + this.artemis = artemis + artemis.start() + } + + override fun stop() = close() + + override fun close() { + super.close() + queueNamesToBridgesMap.exclusive { + for (bridge in values.flatten()) { + bridge.stop() + } + clear() + artemis?.stop() + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/MessagingServerConnectionConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/MessagingServerConnectionConfiguration.kt new file mode 100644 index 0000000000..2b1bf7829a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/MessagingServerConnectionConfiguration.kt @@ -0,0 +1,63 @@ +package net.corda.nodeapi.internal.config + +import net.corda.core.utilities.minutes +import net.corda.core.utilities.seconds +import java.time.Duration + +/** + * Predefined connection configurations used by Artemis clients (currently used in the P2P messaging layer). + * The enum names represent the approximate total duration of the failover (with exponential back-off). The formula used to calculate + * this duration is as follows: + * + * totalFailoverDuration = SUM(k=0 to [reconnectAttempts]) of [retryInterval] * POW([retryIntervalMultiplier], k) + * + * Example calculation for [DEFAULT]: + * + * totalFailoverDuration = 5 + 5 * 1.5 + 5 * (1.5)^2 + 5 * (1.5)^3 + 5 * (1.5)^4 = ~66 seconds + * + * @param failoverOnInitialAttempt Determines whether failover is triggered if initial connection fails. + * @param initialConnectAttempts The number of reconnect attempts if failover is enabled for initial connection. A value + * of -1 represents infinite attempts. + * @param reconnectAttempts The number of reconnect attempts for failover after initial connection is done. A value + * of -1 represents infinite attempts. + * @param retryInterval Duration between reconnect attempts. + * @param retryIntervalMultiplier Value used in the reconnection back-off process. + * @param maxRetryInterval Determines the maximum duration between reconnection attempts. Useful when using infinite retries. + */ +enum class MessagingServerConnectionConfiguration { + + DEFAULT { + override fun failoverOnInitialAttempt(isHa: Boolean) = true + override fun initialConnectAttempts(isHa: Boolean) = 5 + override fun reconnectAttempts(isHa: Boolean) = 5 + override fun retryInterval() = 5.seconds + override fun retryIntervalMultiplier() = 1.5 + override fun maxRetryInterval(isHa: Boolean) = 3.minutes + }, + + FAIL_FAST { + override fun failoverOnInitialAttempt(isHa: Boolean) = isHa + override fun initialConnectAttempts(isHa: Boolean) = 0 + // Client die too fast during failover/failback, need a few reconnect attempts to allow new master to become active + override fun reconnectAttempts(isHa: Boolean) = if (isHa) 3 else 0 + override fun retryInterval() = 5.seconds + override fun retryIntervalMultiplier() = 1.5 + override fun maxRetryInterval(isHa: Boolean) = 3.minutes + }, + + CONTINUOUS_RETRY { + override fun failoverOnInitialAttempt(isHa: Boolean) = true + override fun initialConnectAttempts(isHa: Boolean) = if (isHa) 0 else -1 + override fun reconnectAttempts(isHa: Boolean) = -1 + override fun retryInterval() = 5.seconds + override fun retryIntervalMultiplier() = 1.5 + override fun maxRetryInterval(isHa: Boolean) = if (isHa) 3.minutes else 5.minutes + }; + + abstract fun failoverOnInitialAttempt(isHa: Boolean): Boolean + abstract fun initialConnectAttempts(isHa: Boolean): Int + abstract fun reconnectAttempts(isHa: Boolean): Int + abstract fun retryInterval(): Duration + abstract fun retryIntervalMultiplier(): Double + abstract fun maxRetryInterval(isHa: Boolean): Duration +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt index 1f7d9f398f..0e88c263e9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt @@ -45,7 +45,11 @@ internal class ConnectionStateMachine(private val serverMode: Boolean, userName: String?, password: String?) : BaseHandler() { companion object { - private const val IDLE_TIMEOUT = 10000 + private const val CORDA_AMQP_FRAME_SIZE_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpMaxFrameSize" + private const val CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpIdleTimeout" + + private val MAX_FRAME_SIZE = Integer.getInteger(CORDA_AMQP_FRAME_SIZE_PROP_NAME, 128 * 1024) + private val IDLE_TIMEOUT = Integer.getInteger(CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME, 10 * 1000) private val log = contextLogger() } @@ -102,6 +106,7 @@ internal class ConnectionStateMachine(private val serverMode: Boolean, transport.context = connection @Suppress("UsePropertyAccessSyntax") transport.setEmitFlowEventOnSend(true) + transport.maxFrameSize = MAX_FRAME_SIZE connection.collect(collector) val sasl = transport.sasl() if (userName != null) { @@ -488,7 +493,9 @@ internal class ConnectionStateMachine(private val serverMode: Boolean, } fun transportWriteMessage(msg: SendableMessageImpl) { - msg.buf = encodePayloadBytes(msg) + val encoded = encodePayloadBytes(msg) + msg.release() + msg.buf = encoded val messageQueue = messageQueues.getOrPut(msg.topic, { LinkedList() }) messageQueue.offer(msg) if (session != null) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt index b91642a840..8df0aa0f37 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt @@ -38,7 +38,9 @@ internal class EventProcessor(private val channel: Channel, userName: String?, password: String?) { companion object { - private const val FLOW_WINDOW_SIZE = 10 + private const val CORDA_AMQP_FLOW_WINDOW_SIZE_PROP_NAME = "net.corda.nodeapi.eventprocessor.FlowWindowSize" + + private val FLOW_WINDOW_SIZE = Integer.getInteger(CORDA_AMQP_FLOW_WINDOW_SIZE_PROP_NAME, 5) private val log = contextLogger() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt index 03911bb50f..18c5f3b61e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt @@ -11,4 +11,5 @@ interface ApplicationMessage { val destinationLegalName: String val destinationLink: NetworkHostAndPort val applicationProperties: Map + fun release() } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt index 7bf83505c5..3e95b2fb8c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.messages.impl import io.netty.channel.Channel import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage import org.apache.qpid.proton.engine.Delivery @@ -10,7 +11,7 @@ import org.apache.qpid.proton.engine.Delivery * An internal packet management class that allows tracking of asynchronous acknowledgements * that in turn send Delivery messages back to the originator. */ -internal class ReceivedMessageImpl(override val payload: ByteArray, +internal class ReceivedMessageImpl(override var payload: ByteArray, override val topic: String, override val sourceLegalName: String, override val sourceLink: NetworkHostAndPort, @@ -19,11 +20,25 @@ internal class ReceivedMessageImpl(override val payload: ByteArray, override val applicationProperties: Map, private val channel: Channel, private val delivery: Delivery) : ReceivedMessage { + companion object { + private val emptyPayload = ByteArray(0) + private val logger = contextLogger() + } + data class MessageCompleter(val status: MessageStatus, val delivery: Delivery) + override fun release() { + payload = emptyPayload + } + override fun complete(accepted: Boolean) { + release() val status = if (accepted) MessageStatus.Acknowledged else MessageStatus.Rejected - channel.writeAndFlush(MessageCompleter(status, delivery)) + if (channel.isActive) { + channel.writeAndFlush(MessageCompleter(status, delivery)) + } else { + logger.info("Not writing $status as $channel is not active") + } } override fun toString(): String = "Received ${String(payload)} $topic" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt index 6adc9b2bbc..addff653d1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt @@ -11,11 +11,15 @@ import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage * An internal packet management class that allows handling of the encoded buffers and * allows registration of an acknowledgement handler when the remote receiver confirms durable storage. */ -internal class SendableMessageImpl(override val payload: ByteArray, +internal class SendableMessageImpl(override var payload: ByteArray, override val topic: String, override val destinationLegalName: String, override val destinationLink: NetworkHostAndPort, override val applicationProperties: Map) : SendableMessage { + companion object { + private val emptyPayload = ByteArray(0) + } + var buf: ByteBuf? = null @Volatile var status: MessageStatus = MessageStatus.Unsent @@ -23,12 +27,14 @@ internal class SendableMessageImpl(override val payload: ByteArray, private val _onComplete = openFuture() override val onComplete: CordaFuture get() = _onComplete - fun release() { + override fun release() { + payload = emptyPayload buf?.release() buf = null } fun doComplete(status: MessageStatus) { + release() this.status = status _onComplete.set(status) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt index 273731c891..904c2f9c4d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt @@ -5,11 +5,16 @@ import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import io.netty.channel.socket.SocketChannel +import io.netty.handler.proxy.ProxyConnectException +import io.netty.handler.proxy.ProxyConnectionEvent +import io.netty.handler.ssl.SniCompletionEvent import io.netty.handler.ssl.SslHandler import io.netty.handler.ssl.SslHandshakeCompletionEvent import io.netty.util.ReferenceCountUtil import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.trace +import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY import net.corda.nodeapi.internal.crypto.x509 import net.corda.nodeapi.internal.protonwrapper.engine.EventProcessor import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage @@ -23,6 +28,8 @@ import org.slf4j.MDC import java.net.InetSocketAddress import java.nio.channels.ClosedChannelException import java.security.cert.X509Certificate +import javax.net.ssl.ExtendedSSLSession +import javax.net.ssl.SNIHostName import javax.net.ssl.SSLException /** @@ -30,23 +37,29 @@ import javax.net.ssl.SSLException * It also add some extra checks to the SSL handshake to support our non-standard certificate checks of legal identity. * When a valid SSL connections is made then it initialises a proton-j engine instance to handle the protocol layer. */ +@Suppress("TooManyFunctions") internal class AMQPChannelHandler(private val serverMode: Boolean, private val allowedRemoteLegalNames: Set?, + private val keyManagerFactoriesMap: Map, private val userName: String?, private val password: String?, private val trace: Boolean, - private val onOpen: (Pair) -> Unit, - private val onClose: (Pair) -> Unit, + private val suppressLogs: Boolean, + private val onOpen: (SocketChannel, ConnectionChange) -> Unit, + private val onClose: (SocketChannel, ConnectionChange) -> Unit, private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() { companion object { private val log = contextLogger() + const val PROXY_LOGGER_NAME = "preProxyLogger" } private lateinit var remoteAddress: InetSocketAddress - private var localCert: X509Certificate? = null private var remoteCert: X509Certificate? = null private var eventProcessor: EventProcessor? = null + private var suppressClose: Boolean = false private var badCert: Boolean = false + private var localCert: X509Certificate? = null + private var requestedServerName: String? = null private fun withMDC(block: () -> Unit) { val oldMDC = MDC.getCopyOfContextMap() ?: emptyMap() @@ -62,39 +75,50 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, } } - private fun logDebugWithMDC(msg: () -> String) { - if (log.isDebugEnabled) { - withMDC { log.debug(msg()) } + private fun logDebugWithMDC(msgFn: () -> String) { + if (!suppressLogs) { + if (log.isDebugEnabled) { + withMDC { log.debug(msgFn()) } + } + } else { + withMDC { log.trace(msgFn) } } } - private fun logInfoWithMDC(msg: String) = withMDC { log.info(msg) } + private fun logInfoWithMDC(msgFn: () -> String) { + if (!suppressLogs) { + if (log.isInfoEnabled) { + withMDC { log.info(msgFn()) } + } + } else { + withMDC { log.trace(msgFn) } + } + } - private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) } - - private fun logErrorWithMDC(msg: String, ex: Throwable? = null) = withMDC { log.error(msg, ex) } + private fun logWarnWithMDC(msg: String) = withMDC { if (!suppressLogs) log.warn(msg) else log.trace { msg } } + private fun logErrorWithMDC(msg: String, ex: Throwable? = null) = withMDC { if (!suppressLogs) log.error(msg, ex) else log.trace(msg, ex) } override fun channelActive(ctx: ChannelHandlerContext) { val ch = ctx.channel() remoteAddress = ch.remoteAddress() as InetSocketAddress val localAddress = ch.localAddress() as InetSocketAddress - logInfoWithMDC("New client connection ${ch.id()} from $remoteAddress to $localAddress") + logInfoWithMDC { "New client connection ${ch.id()} from $remoteAddress to $localAddress" } } private fun createAMQPEngine(ctx: ChannelHandlerContext) { val ch = ctx.channel() eventProcessor = EventProcessor(ch, serverMode, localCert!!.subjectX500Principal.toString(), remoteCert!!.subjectX500Principal.toString(), userName, password) - val connection = eventProcessor!!.connection - val transport = connection.transport as ProtonJTransport if (trace) { + val connection = eventProcessor!!.connection + val transport = connection.transport as ProtonJTransport transport.protocolTracer = object : ProtocolTracer { override fun sentFrame(transportFrame: TransportFrame) { - logInfoWithMDC("${transportFrame.body}") + logInfoWithMDC { "${transportFrame.body}" } } override fun receivedFrame(transportFrame: TransportFrame) { - logInfoWithMDC("${transportFrame.body}") + logInfoWithMDC { "${transportFrame.body}" } } } } @@ -104,51 +128,60 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, override fun channelInactive(ctx: ChannelHandlerContext) { val ch = ctx.channel() - logInfoWithMDC("Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}") - onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false, badCert))) + logInfoWithMDC { "Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}" } + if (!suppressClose) { + onClose(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false, badCert)) + } eventProcessor?.close() ctx.fireChannelInactive() } override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { - if (evt is SslHandshakeCompletionEvent) { - if (evt.isSuccess) { - val sslHandler = ctx.pipeline().get(SslHandler::class.java) - localCert = sslHandler.engine().session.localCertificates[0].x509 - remoteCert = sslHandler.engine().session.peerCertificates[0].x509 - val remoteX500Name = try { - CordaX500Name.build(remoteCert!!.subjectX500Principal) - } catch (ex: IllegalArgumentException) { - badCert = true - logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex) - ctx.close() - return + when (evt) { + is ProxyConnectionEvent -> { + if (trace) { + log.info("ProxyConnectionEvent received: $evt") + try { + ctx.pipeline().remove(PROXY_LOGGER_NAME) + } catch (ex: NoSuchElementException) { + // ignore + } } - if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) { - badCert = true - logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames") - ctx.close() - return - } - logInfoWithMDC("Handshake completed with subject: $remoteX500Name") - createAMQPEngine(ctx) - onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true, false))) - } else { - val cause = evt.cause() - // This happens when the peer node is closed during SSL establishment. - if (cause is ClosedChannelException) { - logWarnWithMDC("SSL Handshake closed early.") - } else if (cause is SSLException && cause.message == "handshake timed out") { // Sadly the exception thrown by Netty wrapper requires that we check the message. - logWarnWithMDC("SSL Handshake timed out") - } else { - badCert = true - } - logErrorWithMDC("Handshake failure ${evt.cause().message}") - if (log.isTraceEnabled) { - withMDC { log.trace("Handshake failure", evt.cause()) } - } - ctx.close() + // update address to the real target address + remoteAddress = evt.destinationAddress() } + is SniCompletionEvent -> { + if (evt.isSuccess) { + // The SniCompletionEvent is fired up before context is switched (after SslHandshakeCompletionEvent) + // so we save the requested server name now to be able log it once the handshake is completed successfully + // Note: this event is only triggered when using OpenSSL. + requestedServerName = evt.hostname() + logInfoWithMDC { "SNI completion success." } + } else { + logErrorWithMDC("SNI completion failure: ${evt.cause().message}") + } + } + is SslHandshakeCompletionEvent -> { + if (evt.isSuccess) { + handleSuccessfulHandshake(ctx) + } else { + handleFailedHandshake(ctx, evt) + } + } + } + } + + private fun SslHandler.getRequestedServerName(): String? { + return if (serverMode) { + val session = engine().session + when (session) { + // Server name can be obtained from SSL session when using JavaSSL. + is ExtendedSSLSession -> (session.requestedServerNames.firstOrNull() as? SNIHostName)?.asciiName + // For Open SSL server name is obtained from SniCompletionEvent + else -> requestedServerName + } + } else { + (engine().sslParameters?.serverNames?.firstOrNull() as? SNIHostName)?.asciiName } } @@ -158,6 +191,10 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, if (log.isTraceEnabled) { withMDC { log.trace("Pipeline uncaught exception", cause) } } + if (cause is ProxyConnectException) { + log.warn("Proxy connection failed ${cause.message}") + suppressClose = true // The pipeline gets marked as active on connection to the proxy rather than to the target, which causes excess close events + } ctx.close() } @@ -176,27 +213,27 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, try { try { when (msg) { - // Transfers application packet into the AMQP engine. + // Transfers application packet into the AMQP engine. is SendableMessageImpl -> { val inetAddress = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port) - logDebugWithMDC { "Message for endpoint $inetAddress , expected $remoteAddress "} + logDebugWithMDC { "Message for endpoint $inetAddress , expected $remoteAddress " } require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert!!.subjectX500Principal)) { "Message for incorrect legal identity ${msg.destinationLegalName} expected ${remoteCert!!.subjectX500Principal}" } - logDebugWithMDC { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" } + logDebugWithMDC { "channel write ${msg.applicationProperties[MESSAGE_ID_KEY]}" } eventProcessor!!.transportWriteMessage(msg) } - // A received AMQP packet has been completed and this self-posted packet will be signalled out to the - // external application. + // A received AMQP packet has been completed and this self-posted packet will be signalled out to the + // external application. is ReceivedMessage -> { onReceive(msg) } - // A general self-posted event that triggers creation of AMQP frames when required. + // A general self-posted event that triggers creation of AMQP frames when required. is Transport -> { eventProcessor!!.transportProcessOutput(ctx) } - // A self-posted event that forwards status updates for delivered packets to the application. + // A self-posted event that forwards status updates for delivered packets to the application. is ReceivedMessageImpl.MessageCompleter -> { eventProcessor!!.complete(msg) } @@ -210,4 +247,65 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, } eventProcessor!!.processEventsAsync() } + + private fun handleSuccessfulHandshake(ctx: ChannelHandlerContext) { + val sslHandler = ctx.pipeline().get(SslHandler::class.java) + val sslSession = sslHandler.engine().session + // Depending on what matching method is used, getting the local certificate is done by selecting the + // appropriate keyManagerFactory + val keyManagerFactory = requestedServerName?.let { + keyManagerFactoriesMap[it] + } ?: keyManagerFactoriesMap.values.single() + + localCert = keyManagerFactory.getCurrentCertChain()?.first() + + if (localCert == null) { + log.error("SSL KeyManagerFactory failed to provide a local cert") + ctx.close() + return + } + if (sslSession.peerCertificates == null || sslSession.peerCertificates.isEmpty()) { + log.error("No peer certificates") + ctx.close() + return + } + remoteCert = sslHandler.engine().session.peerCertificates.first().x509 + val remoteX500Name = try { + CordaX500Name.build(remoteCert!!.subjectX500Principal) + } catch (ex: IllegalArgumentException) { + badCert = true + logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex) + ctx.close() + return + } + if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) { + badCert = true + logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames") + ctx.close() + return + } + + logInfoWithMDC { "Handshake completed with subject: $remoteX500Name, requested server name: ${sslHandler.getRequestedServerName()}." } + createAMQPEngine(ctx) + onOpen(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, connected = true, badCert = false)) + } + + private fun handleFailedHandshake(ctx: ChannelHandlerContext, evt: SslHandshakeCompletionEvent) { + val cause = evt.cause() + // This happens when the peer node is closed during SSL establishment. + when { + cause is ClosedChannelException -> logWarnWithMDC("SSL Handshake closed early.") + // Sadly the exception thrown by Netty wrapper requires that we check the message. + cause is SSLException && cause.message == "handshake timed out" -> logWarnWithMDC("SSL Handshake timed out") + cause is SSLException && (cause.message?.contains("close_notify") == true) + -> logWarnWithMDC("Received close_notify during handshake") + + else -> badCert = true + } + logWarnWithMDC("Handshake failure: ${evt.cause().message}") + if (log.isTraceEnabled) { + withMDC { log.trace("Handshake failure", evt.cause()) } + } + ctx.close() + } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt index a9f16b8e77..561119b4f5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt @@ -7,24 +7,45 @@ import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler +import io.netty.handler.proxy.HttpProxyHandler +import io.netty.handler.proxy.Socks4ProxyHandler +import io.netty.handler.proxy.Socks5ProxyHandler +import io.netty.resolver.NoopAddressResolverGroup import io.netty.util.internal.logging.InternalLoggerFactory import io.netty.util.internal.logging.Slf4JLoggerFactory import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPChannelHandler.Companion.PROXY_LOGGER_NAME import net.corda.nodeapi.internal.requireMessageSize import rx.Observable import rx.subjects.PublishSubject import java.lang.Long.min +import java.net.InetSocketAddress import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import javax.net.ssl.KeyManagerFactory import javax.net.ssl.TrustManagerFactory import kotlin.concurrent.withLock +enum class ProxyVersion { + SOCKS4, + SOCKS5, + HTTP +} + +data class ProxyConfig(val version: ProxyVersion, val proxyAddress: NetworkHostAndPort, val userName: String? = null, val password: String? = null, val proxyTimeoutMS: Long? = null) { + init { + if (version == ProxyVersion.SOCKS4) { + require(password == null) { "SOCKS4 does not support a password" } + } + } +} + /** * The AMQPClient creates a connection initiator that will try to connect in a round-robin fashion * to the first open SSL socket. It will keep retrying until it is stopped. @@ -42,15 +63,18 @@ class AMQPClient(val targets: List, } val log = contextLogger() - const val MIN_RETRY_INTERVAL = 1000L - const val MAX_RETRY_INTERVAL = 60000L - const val BACKOFF_MULTIPLIER = 2L - const val NUM_CLIENT_THREADS = 2 + + private const val CORDA_AMQP_NUM_CLIENT_THREAD_PROP_NAME = "net.corda.nodeapi.amqpclient.NumClientThread" + + private const val MIN_RETRY_INTERVAL = 1000L + private const val MAX_RETRY_INTERVAL = 60000L + private const val BACKOFF_MULTIPLIER = 2L + private val NUM_CLIENT_THREADS = Integer.getInteger(CORDA_AMQP_NUM_CLIENT_THREAD_PROP_NAME, 2) } private val lock = ReentrantLock() @Volatile - private var stopping: Boolean = false + private var started: Boolean = false private var workerGroup: EventLoopGroup? = null @Volatile private var clientChannel: Channel? = null @@ -59,6 +83,13 @@ class AMQPClient(val targets: List, private var currentTarget: NetworkHostAndPort = targets.first() private var retryInterval = MIN_RETRY_INTERVAL private val badCertTargets = mutableSetOf() + @Volatile + private var amqpActive = false + @Volatile + private var amqpChannelHandler: ChannelHandler? = null + + val localAddressString: String + get() = clientChannel?.localAddress()?.toString() ?: "" private fun nextTarget() { val origIndex = targetIndex @@ -80,29 +111,31 @@ class AMQPClient(val targets: List, private val connectListener = object : ChannelFutureListener { override fun operationComplete(future: ChannelFuture) { + amqpActive = false if (!future.isSuccess) { log.info("Failed to connect to $currentTarget") - if (!stopping) { + if (started) { workerGroup?.schedule({ nextTarget() restart() }, retryInterval, TimeUnit.MILLISECONDS) } } else { - log.info("Connected to $currentTarget") // Connection established successfully clientChannel = future.channel() clientChannel?.closeFuture()?.addListener(closeListener) + log.info("Connected to $currentTarget, Local address: $localAddressString") } } } private val closeListener = ChannelFutureListener { future -> - log.info("Disconnected from $currentTarget") + log.info("Disconnected from $currentTarget, Local address: $localAddressString") future.channel()?.disconnect() clientChannel = null - if (!stopping) { + if (started && !amqpActive) { + log.debug { "Scheduling restart of $currentTarget (AMQP inactive)" } workerGroup?.schedule({ nextTarget() restart() @@ -114,42 +147,110 @@ class AMQPClient(val targets: List, private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) private val conf = parent.configuration + @Volatile + private lateinit var amqpChannelHandler: AMQPChannelHandler init { keyManagerFactory.init(conf.keyStore) - trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig)) } + @Suppress("ComplexMethod") override fun initChannel(ch: SocketChannel) { val pipeline = ch.pipeline() + val proxyConfig = conf.proxyConfig + if (proxyConfig != null) { + if (conf.trace) pipeline.addLast(PROXY_LOGGER_NAME, LoggingHandler(LogLevel.INFO)) + val proxyAddress = InetSocketAddress(proxyConfig.proxyAddress.host, proxyConfig.proxyAddress.port) + val proxy = when (conf.proxyConfig!!.version) { + ProxyVersion.SOCKS4 -> { + Socks4ProxyHandler(proxyAddress, proxyConfig.userName) + } + ProxyVersion.SOCKS5 -> { + Socks5ProxyHandler(proxyAddress, proxyConfig.userName, proxyConfig.password) + } + ProxyVersion.HTTP -> { + val httpProxyHandler = if(proxyConfig.userName == null || proxyConfig.password == null) { + HttpProxyHandler(proxyAddress) + } else { + HttpProxyHandler(proxyAddress, proxyConfig.userName, proxyConfig.password) + } + //httpProxyHandler.setConnectTimeoutMillis(3600000) // 1hr for debugging purposes + httpProxyHandler + } + } + val proxyTimeout = proxyConfig.proxyTimeoutMS + if (proxyTimeout != null) { + proxy.setConnectTimeoutMillis(proxyTimeout) + } + pipeline.addLast("Proxy", proxy) + proxy.connectFuture().addListener { + if (!it.isSuccess) { + ch.disconnect() + } + } + } + + val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, parent.configuration) val target = parent.currentTarget - val handler = createClientSslHelper(target, parent.allowedRemoteLegalNames, keyManagerFactory, trustManagerFactory) + val handler = if (parent.configuration.useOpenSsl) { + createClientOpenSslHandler(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory, ch.alloc()) + } else { + createClientSslHelper(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory) + } + handler.handshakeTimeoutMillis = conf.sslHandshakeTimeout pipeline.addLast("sslHandler", handler) if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO)) - pipeline.addLast(AMQPChannelHandler(false, + amqpChannelHandler = AMQPChannelHandler(false, parent.allowedRemoteLegalNames, + // Single entry, key can be anything. + mapOf(DEFAULT to wrappedKeyManagerFactory), conf.userName, conf.password, conf.trace, - { - parent.retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly - parent._onConnection.onNext(it.second) - }, - { - parent._onConnection.onNext(it.second) - if (it.second.badCert) { - log.error("Blocking future connection attempts to $target due to bad certificate on endpoint") - parent.badCertTargets += target + false, + onOpen = { _, change -> + parent.run { + amqpActive = true + retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly + _onConnection.onNext(change) } }, - { rcv -> parent._onReceive.onNext(rcv) })) + onClose = { _, change -> + if (parent.amqpChannelHandler == amqpChannelHandler) { + parent.run { + _onConnection.onNext(change) + if (change.badCert) { + log.error("Blocking future connection attempts to $target due to bad certificate on endpoint") + badCertTargets += target + } + + if (started && amqpActive) { + log.debug { "Scheduling restart of $currentTarget (AMQP active)" } + workerGroup?.schedule({ + nextTarget() + restart() + }, retryInterval, TimeUnit.MILLISECONDS) + } + amqpActive = false + } + } + }, + onReceive = { rcv -> parent._onReceive.onNext(rcv) }) + parent.amqpChannelHandler = amqpChannelHandler + pipeline.addLast(amqpChannelHandler) } } fun start() { lock.withLock { - log.info("connect to: $currentTarget") + if (started) { + log.info("Already connected to: $currentTarget so returning") + return + } + log.info("Connect to: $currentTarget") workerGroup = sharedThreadPool ?: NioEventLoopGroup(NUM_CLIENT_THREADS) + started = true restart() } } @@ -161,6 +262,10 @@ class AMQPClient(val targets: List, val bootstrap = Bootstrap() // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux bootstrap.group(workerGroup).channel(NioSocketChannel::class.java).handler(ClientChannelInitializer(this)) + // Delegate DNS Resolution to the proxy side, if we are using proxy. + if (configuration.proxyConfig != null) { + bootstrap.resolver(NoopAddressResolverGroup.INSTANCE) + } currentTarget = targets[targetIndex] val clientFuture = bootstrap.connect(currentTarget.host, currentTarget.port) clientFuture.addListener(connectListener) @@ -168,21 +273,17 @@ class AMQPClient(val targets: List, fun stop() { lock.withLock { - log.info("disconnect from: $currentTarget") - stopping = true - try { - if (sharedThreadPool == null) { - workerGroup?.shutdownGracefully() - workerGroup?.terminationFuture()?.sync() - } else { - clientChannel?.close()?.sync() - } - clientChannel = null - workerGroup = null - } finally { - stopping = false + log.info("Stopping connection to: $currentTarget, Local address: $localAddressString") + started = false + if (sharedThreadPool == null) { + workerGroup?.shutdownGracefully() + workerGroup?.terminationFuture()?.sync() + } else { + clientChannel?.close()?.sync() } - log.info("stopped connection to $currentTarget") + clientChannel = null + workerGroup = null + log.info("Stopped connection to $currentTarget") } } @@ -191,7 +292,7 @@ class AMQPClient(val targets: List, val connected: Boolean get() { val channel = lock.withLock { clientChannel } - return channel?.isActive ?: false + return isChannelWritable(channel) } fun createMessage(payload: ByteArray, @@ -204,13 +305,17 @@ class AMQPClient(val targets: List, fun write(msg: SendableMessage) { val channel = clientChannel - if (channel == null) { + if (channel == null || !isChannelWritable(channel)) { throw IllegalStateException("Connection to $targets not active") } else { channel.writeAndFlush(msg) } } + private fun isChannelWritable(channel: Channel?): Boolean { + return channel?.let { channel.isOpen && channel.isActive && amqpActive } ?: false + } + private val _onReceive = PublishSubject.create().toSerialized() val onReceive: Observable get() = _onReceive diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt index 3b7289a8c5..db0dd8023c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt @@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.config.CertificateStore -import java.security.KeyStore +import net.corda.nodeapi.internal.config.DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS interface AMQPConfiguration { /** @@ -32,12 +32,11 @@ interface AMQPConfiguration { val trustStore: CertificateStore /** - * Setting crlCheckSoftFail to true allows certificate paths where some leaf certificates do not contain cRLDistributionPoints - * and also allows validation to continue if the CRL distribution server is not contactable. + * Control how CRL check will be performed. */ @JvmDefault - val crlCheckSoftFail: Boolean - get() = true + val revocationConfig: RevocationConfig + get() = RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) /** * Enables full debug tracing of all netty and AMQP level packets. This logs aat very high volume and is only for developers. @@ -51,5 +50,41 @@ interface AMQPConfiguration { * but currently that is deferred to Artemis and the bridge code. */ val maxMessageSize: Int + + @JvmDefault + val proxyConfig: ProxyConfig? + get() = null + + @JvmDefault + val sourceX500Name: String? + get() = null + + /** + * Whether to use the tcnative open/boring SSL provider or the default Java SSL provider + */ + @JvmDefault + val useOpenSsl: Boolean + get() = false + + @JvmDefault + val sslHandshakeTimeout: Long + get() = DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT + + /** + * An optional Health Check Phrase which if passed through the channel will cause AMQP Server to echo it back instead of doing normal pipeline processing + */ + val healthCheckPhrase: String? + get() = null + + /** + * An optional set of IPv4/IPv6 remote address strings which will be compared to the remote address of inbound connections and these will only log at TRACE level + */ + @JvmDefault + val silencedIPs: Set + get() = emptySet() + + @JvmDefault + val enableSNI: Boolean + get() = true } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 56c8b8bfda..20834a2041 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty import io.netty.bootstrap.ServerBootstrap import io.netty.channel.Channel +import io.netty.channel.ChannelHandler import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelOption import io.netty.channel.EventLoopGroup @@ -14,6 +15,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory import io.netty.util.internal.logging.Slf4JLoggerFactory import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl @@ -31,7 +33,6 @@ import kotlin.concurrent.withLock /** * This create a socket acceptor instance that can receive possibly multiple AMQP connections. - * As of now this is not used outside of testing, but in future it will be used for standalone bridging components. */ class AMQPServer(val hostName: String, val port: Int, @@ -42,8 +43,10 @@ class AMQPServer(val hostName: String, InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE) } + private const val CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME = "net.corda.nodeapi.amqpserver.NumServerThreads" + private val log = contextLogger() - const val NUM_SERVER_THREADS = 4 + private val NUM_SERVER_THREADS = Integer.getInteger(CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME, 4) } private val lock = ReentrantLock() @@ -60,29 +63,59 @@ class AMQPServer(val hostName: String, private val conf = parent.configuration init { - keyManagerFactory.init(conf.keyStore) - trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) + keyManagerFactory.init(conf.keyStore.value.internal, conf.keyStore.entryPassword.toCharArray()) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig)) } override fun initChannel(ch: SocketChannel) { + val amqpConfiguration = parent.configuration val pipeline = ch.pipeline() - val handler = createServerSslHelper(keyManagerFactory, trustManagerFactory) - pipeline.addLast("sslHandler", handler) + amqpConfiguration.healthCheckPhrase?.let { pipeline.addLast(ModeSelectingChannel.NAME, ModeSelectingChannel(it)) } + val (sslHandler, keyManagerFactoriesMap) = createSSLHandler(amqpConfiguration, ch) + pipeline.addLast("sslHandler", sslHandler) if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO)) + val suppressLogs = ch.remoteAddress()?.hostString in amqpConfiguration.silencedIPs pipeline.addLast(AMQPChannelHandler(true, null, + // Passing a mapping of legal names to key managers to be able to pick the correct one after + // SNI completion event is fired up. + keyManagerFactoriesMap, conf.userName, conf.password, conf.trace, - { - parent.clientChannels[it.first.remoteAddress()] = it.first - parent._onConnection.onNext(it.second) + suppressLogs, + onOpen = { channel, change -> + parent.run { + clientChannels[channel.remoteAddress()] = channel + _onConnection.onNext(change) + } }, - { - parent.clientChannels.remove(it.first.remoteAddress()) - parent._onConnection.onNext(it.second) + onClose = { channel, change -> + parent.run { + val remoteAddress = channel.remoteAddress() + clientChannels.remove(remoteAddress) + _onConnection.onNext(change) + } }, - { rcv -> parent._onReceive.onNext(rcv) })) + onReceive = { rcv -> parent._onReceive.onNext(rcv) })) + } + + private fun createSSLHandler(amqpConfig: AMQPConfiguration, ch: SocketChannel): Pair> { + return if (amqpConfig.useOpenSsl && amqpConfig.enableSNI && amqpConfig.keyStore.aliases().size > 1) { + val keyManagerFactoriesMap = splitKeystore(amqpConfig) + // SNI matching needed only when multiple nodes exist behind the server. + Pair(createServerSNIOpenSslHandler(keyManagerFactoriesMap, trustManagerFactory), keyManagerFactoriesMap) + } else { + val keyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, amqpConfig) + val handler = if (amqpConfig.useOpenSsl) { + createServerOpenSslHandler(keyManagerFactory, trustManagerFactory, ch.alloc()) + } else { + // For javaSSL, SNI matching is handled at key manager level. + createServerSslHandler(amqpConfig.keyStore, keyManagerFactory, trustManagerFactory) + } + handler.handshakeTimeoutMillis = amqpConfig.sslHandshakeTimeout + Pair(handler, mapOf(DEFAULT to keyManagerFactory)) + } } } @@ -95,7 +128,10 @@ class AMQPServer(val hostName: String, val server = ServerBootstrap() // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux - server.group(bossGroup, workerGroup).channel(NioServerSocketChannel::class.java).option(ChannelOption.SO_BACKLOG, 100).handler(LoggingHandler(LogLevel.INFO)).childHandler(ServerChannelInitializer(this)) + server.group(bossGroup, workerGroup).channel(NioServerSocketChannel::class.java) + .option(ChannelOption.SO_BACKLOG, 100) + .handler(NettyServerEventLogger(LogLevel.INFO, configuration.silencedIPs)) + .childHandler(ServerChannelInitializer(this)) log.info("Try to bind $port") val channelFuture = server.bind(hostName, port).sync() // block/throw here as better to know we failed to claim port than carry on @@ -144,7 +180,7 @@ class AMQPServer(val hostName: String, requireMessageSize(payload.size, configuration.maxMessageSize) val dest = InetSocketAddress(destinationLink.host, destinationLink.port) require(dest in clientChannels.keys) { - "Destination not available" + "Destination $dest is not available" } return SendableMessageImpl(payload, topic, destinationLegalName, destinationLink, properties) } @@ -155,21 +191,22 @@ class AMQPServer(val hostName: String, if (channel == null) { throw IllegalStateException("Connection to ${msg.destinationLink} not active") } else { + log.debug { "Writing message with payload of size ${msg.payload.size} into channel $channel" } channel.writeAndFlush(msg) + log.debug { "Done writing message with payload of size ${msg.payload.size} into channel $channel" } } } fun dropConnection(connectionRemoteHost: InetSocketAddress) { - val channel = clientChannels[connectionRemoteHost] - if (channel != null) { - channel.close() - } + clientChannels[connectionRemoteHost]?.close() } fun complete(delivery: Delivery, target: InetSocketAddress) { val channel = clientChannels[target] channel?.apply { + log.debug { "Writing delivery $delivery into channel $channel" } writeAndFlush(delivery) + log.debug { "Done writing delivery $delivery into channel $channel" } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AliasProvidingKeyMangerWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AliasProvidingKeyMangerWrapper.kt new file mode 100644 index 0000000000..4f44869212 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AliasProvidingKeyMangerWrapper.kt @@ -0,0 +1,60 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import java.net.Socket +import java.security.Principal +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509KeyManager + +interface AliasProvidingKeyMangerWrapper : X509KeyManager { + var lastAlias: String? +} + + +class AliasProvidingKeyMangerWrapperImpl(private val keyManager: X509KeyManager) : AliasProvidingKeyMangerWrapper, X509KeyManager by keyManager { + override var lastAlias: String? = null + + override fun chooseServerAlias(keyType: String?, issuers: Array?, socket: Socket?): String? { + return storeIfNotNull { keyManager.chooseServerAlias(keyType, issuers, socket) } + } + + override fun chooseClientAlias(keyType: Array?, issuers: Array?, socket: Socket?): String? { + return storeIfNotNull { keyManager.chooseClientAlias(keyType, issuers, socket) } + } + + private fun storeIfNotNull(func: () -> String?): String? { + val alias = func() + if (alias != null) { + lastAlias = alias + } + return alias + } +} + +class AliasProvidingExtendedKeyMangerWrapper(private val keyManager: X509ExtendedKeyManager) : X509ExtendedKeyManager(), X509KeyManager by keyManager, AliasProvidingKeyMangerWrapper { + override var lastAlias: String? = null + + override fun chooseServerAlias(keyType: String?, issuers: Array?, socket: Socket?): String? { + return storeIfNotNull { keyManager.chooseServerAlias(keyType, issuers, socket) } + } + + override fun chooseClientAlias(keyType: Array?, issuers: Array?, socket: Socket?): String? { + return storeIfNotNull { keyManager.chooseClientAlias(keyType, issuers, socket) } + } + + override fun chooseEngineClientAlias(keyType: Array?, issuers: Array?, engine: SSLEngine?): String? { + return storeIfNotNull { keyManager.chooseEngineClientAlias(keyType, issuers, engine) } + } + + override fun chooseEngineServerAlias(keyType: String?, issuers: Array?, engine: SSLEngine?): String? { + return storeIfNotNull { keyManager.chooseEngineServerAlias(keyType, issuers, engine) } + } + + private fun storeIfNotNull(func: () -> String?): String? { + val alias = func() + if (alias != null) { + lastAlias = alias + } + return alias + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AllowAllRevocationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AllowAllRevocationChecker.kt new file mode 100644 index 0000000000..30e0445689 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AllowAllRevocationChecker.kt @@ -0,0 +1,34 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import net.corda.core.utilities.debug +import org.slf4j.LoggerFactory +import java.security.cert.CertPathValidatorException +import java.security.cert.Certificate +import java.security.cert.PKIXRevocationChecker +import java.util.* + +object AllowAllRevocationChecker : PKIXRevocationChecker() { + + private val logger = LoggerFactory.getLogger(AllowAllRevocationChecker::class.java) + + override fun check(cert: Certificate?, unresolvedCritExts: MutableCollection?) { + logger.debug {"Passing certificate check for: $cert"} + // Nothing to do + } + + override fun isForwardCheckingSupported(): Boolean { + return true + } + + override fun getSupportedExtensions(): MutableSet? { + return null + } + + override fun init(forward: Boolean) { + // Nothing to do + } + + override fun getSoftFailExceptions(): MutableList { + return LinkedList() + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/CertHoldingKeyManagerFactoryWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/CertHoldingKeyManagerFactoryWrapper.kt new file mode 100644 index 0000000000..752b249a71 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/CertHoldingKeyManagerFactoryWrapper.kt @@ -0,0 +1,81 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import java.security.KeyStore +import java.security.cert.X509Certificate +import javax.net.ssl.KeyManager +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.KeyManagerFactorySpi +import javax.net.ssl.ManagerFactoryParameters +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509KeyManager + +class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerFactorySpi, private val amqpConfig: AMQPConfiguration) : KeyManagerFactorySpi() { + override fun engineInit(keyStore: KeyStore?, password: CharArray?) { + val engineInitMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineInit", KeyStore::class.java, CharArray::class.java) + engineInitMethod.isAccessible = true + engineInitMethod.invoke(factorySpi, keyStore, password) + } + + override fun engineInit(spec: ManagerFactoryParameters?) { + val engineInitMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineInit", ManagerFactoryParameters::class.java) + engineInitMethod.isAccessible = true + engineInitMethod.invoke(factorySpi, spec) + } + + private fun getKeyManagersImpl(): Array { + val engineGetKeyManagersMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineGetKeyManagers") + engineGetKeyManagersMethod.isAccessible = true + @Suppress("UNCHECKED_CAST") + val keyManagers = engineGetKeyManagersMethod.invoke(factorySpi) as Array + return if (factorySpi is CertHoldingKeyManagerFactorySpiWrapper) keyManagers else keyManagers.map { + val aliasProvidingKeyManager = getDefaultKeyManager(it) + // Use the SNIKeyManager if keystore has several entries and only for clients and non-openSSL servers. + // Condition of using SNIKeyManager: if its client, or JDKSsl server. + val isClient = amqpConfig.sourceX500Name != null + val enableSNI = amqpConfig.enableSNI && amqpConfig.keyStore.aliases().size > 1 + if (enableSNI && (isClient || !amqpConfig.useOpenSsl)) { + SNIKeyManager(aliasProvidingKeyManager as X509ExtendedKeyManager, amqpConfig) + } else { + aliasProvidingKeyManager + } + }.toTypedArray() + } + + private fun getDefaultKeyManager(keyManager: KeyManager): KeyManager { + return when (keyManager) { + is X509ExtendedKeyManager -> AliasProvidingExtendedKeyMangerWrapper(keyManager) + is X509KeyManager -> AliasProvidingKeyMangerWrapperImpl(keyManager) + else -> throw UnsupportedOperationException("Supported key manager types are: X509ExtendedKeyManager, X509KeyManager. Provided ${keyManager::class.java.name}") + } + } + + private val keyManagers = lazy { getKeyManagersImpl() } + + override fun engineGetKeyManagers(): Array { + return keyManagers.value + } +} + +/** + * You can wrap a key manager factory in this class if you need to get the cert chain currently used to identify or + * verify. When using for TLS channels, make sure to wrap the (singleton) factory separately on each channel, as + * the wrapper is not thread safe as in it will return the last used alias/cert chain and has itself no notion + * of belonging to a certain channel. + */ +class CertHoldingKeyManagerFactoryWrapper(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration) : KeyManagerFactory(getFactorySpi(factory, amqpConfig), factory.provider, factory.algorithm) { + companion object { + private fun getFactorySpi(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration): KeyManagerFactorySpi { + val spiField = KeyManagerFactory::class.java.getDeclaredField("factorySpi") + spiField.isAccessible = true + return CertHoldingKeyManagerFactorySpiWrapper(spiField.get(factory) as KeyManagerFactorySpi, amqpConfig) + } + } + + fun getCurrentCertChain(): Array? { + val keyManager = keyManagers.firstOrNull() + val alias = if (keyManager is AliasProvidingKeyMangerWrapper) keyManager.lastAlias else null + return if (alias != null && keyManager is X509KeyManager) { + keyManager.getCertificateChain(alias) + } else null + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ExternalCrlSource.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ExternalCrlSource.kt new file mode 100644 index 0000000000..654ead24a0 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ExternalCrlSource.kt @@ -0,0 +1,12 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import java.security.cert.X509CRL +import java.security.cert.X509Certificate + +interface ExternalCrlSource { + + /** + * Given certificate provides a set of CRLs, potentially performing remote communication. + */ + fun fetch(certificate: X509Certificate) : Set +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ModeSelectingChannel.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ModeSelectingChannel.kt new file mode 100644 index 0000000000..1966309238 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ModeSelectingChannel.kt @@ -0,0 +1,76 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import io.netty.handler.ssl.SslHandler +import net.corda.core.utilities.contextLogger + +/** + * Responsible for deciding whether we are likely to be processing health probe request + * or this is a normal SSL/AMQP processing pipeline + */ +internal class ModeSelectingChannel(healthCheckPhrase: String) : ByteToMessageDecoder() { + + companion object { + const val NAME = "modeSelector" + private val log = contextLogger() + } + + private enum class TriState { + UNDECIDED, + ECHO_MODE, + NORMAL_MODE + } + + private val healthCheckPhraseArray = healthCheckPhrase.toByteArray(Charsets.UTF_8) + + private var currentMode = TriState.UNDECIDED + + private var alreadyEchoedPos = 0 + + override fun decode(ctx: ChannelHandlerContext, inByteBuf: ByteBuf, out: MutableList?) { + + fun ChannelHandlerContext.echoBack(inByteBuf: ByteBuf) { + + // WriteAndFlush() will decrement count and will blow unless we retain first + // And we have to ensure we are not sending the same information multiple times + val toBeWritten = inByteBuf.retainedSlice(alreadyEchoedPos, inByteBuf.readableBytes() - alreadyEchoedPos) + + writeAndFlush(toBeWritten) + + alreadyEchoedPos = inByteBuf.readableBytes() + } + + if(currentMode == TriState.ECHO_MODE) { + ctx.echoBack(inByteBuf) + return + } + + // Wait until the length prefix is available. + if (inByteBuf.readableBytes() < healthCheckPhraseArray.size) { + return + } + + // Direct buffers do not allow calling `.array()` on them, see `io.netty.buffer.UnpooledDirectByteBuf.array` + val incomingArray = Unpooled.copiedBuffer(inByteBuf).array() + val zipped = healthCheckPhraseArray.zip(incomingArray) + if (zipped.all { it.first == it.second }) { + // Matched the healthCheckPhrase + currentMode = TriState.ECHO_MODE + log.info("Echo mode activated for connection ${ctx.channel().id()}") + // Cancel scheduled action to avoid SSL handshake timeout, which starts "ticking" upon connection is established, + // namely upon call to `io.netty.handler.ssl.SslHandler#handlerAdded` is made + ctx.pipeline().get(SslHandler::class.java)?.handshakeFuture()?.cancel(false) + ctx.echoBack(inByteBuf) + } else { + currentMode = TriState.NORMAL_MODE + // Remove self from pipeline and replay all the messages received down the pipeline + // It is important to bump-up reference count as pipeline removal decrements it by one. + inByteBuf.retain() + ctx.pipeline().remove(this) + ctx.fireChannelRead(inByteBuf) + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/NettyServerEventLogger.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/NettyServerEventLogger.kt new file mode 100644 index 0000000000..d90a2255e2 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/NettyServerEventLogger.kt @@ -0,0 +1,73 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import io.netty.channel.ChannelDuplexHandler +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPromise +import io.netty.handler.logging.LogLevel +import io.netty.util.internal.logging.InternalLogLevel +import io.netty.util.internal.logging.InternalLogger +import io.netty.util.internal.logging.InternalLoggerFactory +import java.net.SocketAddress + +@ChannelHandler.Sharable +class NettyServerEventLogger(level: LogLevel = DEFAULT_LEVEL, val silencedIPs: Set = emptySet()) : ChannelDuplexHandler() { + companion object { + val DEFAULT_LEVEL: LogLevel = LogLevel.DEBUG + } + + private val logger: InternalLogger = InternalLoggerFactory.getInstance(javaClass) + private val internalLevel: InternalLogLevel = level.toInternalLevel() + + @Throws(Exception::class) + override fun channelActive(ctx: ChannelHandlerContext) { + if (logger.isEnabled(internalLevel)) { + logger.log(internalLevel, "Server socket ${ctx.channel()} ACTIVE") + } + ctx.fireChannelActive() + } + + @Throws(Exception::class) + override fun channelInactive(ctx: ChannelHandlerContext) { + if (logger.isEnabled(internalLevel)) { + logger.log(internalLevel, "Server socket ${ctx.channel()} INACTIVE") + } + ctx.fireChannelInactive() + } + + @Suppress("OverridingDeprecatedMember") + @Throws(Exception::class) + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + if (logger.isEnabled(internalLevel)) { + logger.log(internalLevel, "Server socket ${ctx.channel()} EXCEPTION ${cause.message}", cause) + } + ctx.fireExceptionCaught(cause) + } + + @Throws(Exception::class) + override fun bind(ctx: ChannelHandlerContext, localAddress: SocketAddress, promise: ChannelPromise) { + if (logger.isEnabled(internalLevel)) { + logger.log(internalLevel, "Server socket ${ctx.channel()} BIND $localAddress") + } + ctx.bind(localAddress, promise) + } + + @Throws(Exception::class) + override fun close(ctx: ChannelHandlerContext, promise: ChannelPromise) { + if (logger.isEnabled(internalLevel)) { + logger.log(internalLevel, "Server socket ${ctx.channel()} CLOSE") + } + ctx.close(promise) + } + + @Throws(Exception::class) + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + val level = if (msg is io.netty.channel.socket.SocketChannel) { // Should always be the case as this is a server socket, but be defensive + if (msg.remoteAddress()?.hostString !in silencedIPs) internalLevel else InternalLogLevel.TRACE + } else internalLevel + if (logger.isEnabled(level)) { + logger.log(level, "Server socket ${ctx.channel()} ACCEPTED $msg") + } + ctx.fireChannelRead(msg) + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/RevocationConfig.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/RevocationConfig.kt new file mode 100644 index 0000000000..87535f37aa --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/RevocationConfig.kt @@ -0,0 +1,83 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import com.typesafe.config.Config +import net.corda.nodeapi.internal.config.ConfigParser +import net.corda.nodeapi.internal.config.CustomConfigParser + +/** + * Data structure for controlling the way how Certificate Revocation Lists are handled. + */ +@CustomConfigParser(RevocationConfigParser::class) +interface RevocationConfig { + + enum class Mode { + + /** + * @see java.security.cert.PKIXRevocationChecker.Option.SOFT_FAIL + */ + SOFT_FAIL, + + /** + * Opposite of SOFT_FAIL - i.e. most rigorous check. + * Among other things, this check requires that CRL checking URL is available on every level of certificate chain. + * This is also known as Strict mode. + */ + HARD_FAIL, + + /** + * CRLs are obtained from external source + * @see ExternalCrlSource + */ + EXTERNAL_SOURCE, + + /** + * Switch CRL check off. + */ + OFF + } + + val mode: Mode + + /** + * Optional `ExternalCrlSource` which only makes sense with `mode` = `EXTERNAL_SOURCE` + */ + val externalCrlSource: ExternalCrlSource? + + /** + * Creates a copy of `RevocationConfig` with ExternalCrlSource enriched + */ + fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig +} + +/** + * Maintained for legacy purposes to convert old style `crlCheckSoftFail`. + */ +fun Boolean.toRevocationConfig() = if(this) RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) else RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL) + +data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: ExternalCrlSource? = null) : RevocationConfig { + override fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig { + return if(mode != RevocationConfig.Mode.EXTERNAL_SOURCE) { + this + } else { + assert(sourceFunc != null) { "There should be a way to obtain ExternalCrlSource" } + copy(externalCrlSource = sourceFunc!!()) + } + } +} + +class RevocationConfigParser : ConfigParser { + override fun parse(config: Config): RevocationConfig { + val oneAndTheOnly = "mode" + val allKeys = config.entrySet().map { it.key } + require(allKeys.size == 1 && allKeys.contains(oneAndTheOnly)) {"For RevocationConfig, it is expected to have '$oneAndTheOnly' property only. " + + "Actual set of properties: $allKeys. Please check 'revocationConfig' section."} + val mode = config.getString(oneAndTheOnly) + return when (mode.toUpperCase()) { + "SOFT_FAIL" -> RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) + "HARD_FAIL" -> RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL) + "EXTERNAL_SOURCE" -> RevocationConfigImpl(RevocationConfig.Mode.EXTERNAL_SOURCE, null) // null for now till `enrichExternalCrlSource` is called + "OFF" -> RevocationConfigImpl(RevocationConfig.Mode.OFF) + else -> throw IllegalArgumentException("Unsupported mode : '$mode'") + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SNIKeyManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SNIKeyManager.kt new file mode 100644 index 0000000000..e28451fc44 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SNIKeyManager.kt @@ -0,0 +1,112 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.x509 +import org.slf4j.MDC +import java.net.Socket +import java.security.Principal +import javax.net.ssl.SNIMatcher +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSocket +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509KeyManager + +internal class SNIKeyManager(private val keyManager: X509ExtendedKeyManager, private val amqpConfig: AMQPConfiguration) : X509ExtendedKeyManager(), X509KeyManager by keyManager, AliasProvidingKeyMangerWrapper { + + companion object { + private val log = contextLogger() + } + + override var lastAlias: String? = null + + private fun withMDC(block: () -> Unit) { + val oldMDC = MDC.getCopyOfContextMap() + try { + MDC.put("lastAlias", lastAlias) + MDC.put("isServer", amqpConfig.sourceX500Name.isNullOrEmpty().toString()) + MDC.put("sourceX500Name", amqpConfig.sourceX500Name) + MDC.put("useOpenSSL", amqpConfig.useOpenSsl.toString()) + block() + } finally { + MDC.setContextMap(oldMDC) + } + } + + private fun logDebugWithMDC(msg: () -> String) { + if (log.isDebugEnabled) { + withMDC { log.debug(msg()) } + } + } + + override fun chooseClientAlias(keyType: Array, issuers: Array, socket: Socket): String? { + return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) } + } + + override fun chooseEngineClientAlias(keyType: Array, issuers: Array, engine: SSLEngine): String? { + return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) } + } + + override fun chooseServerAlias(keyType: String?, issuers: Array?, socket: Socket): String? { + return storeIfNotNull { + val matcher = (socket as SSLSocket).sslParameters.sniMatchers.first() + chooseServerAlias(keyType, issuers, matcher) + } + } + + override fun chooseEngineServerAlias(keyType: String?, issuers: Array?, engine: SSLEngine?): String? { + return storeIfNotNull { + val matcher = engine?.sslParameters?.sniMatchers?.first() + chooseServerAlias(keyType, issuers, matcher) + } + } + + private fun chooseServerAlias(keyType: String?, issuers: Array?, matcher: SNIMatcher?): String? { + val aliases = keyManager.getServerAliases(keyType, issuers) + if (aliases == null || aliases.isEmpty()) { + logDebugWithMDC { "Keystore doesn't contain any aliases for key type $keyType and issuers $issuers." } + return null + } + + log.debug("Checking aliases: $aliases.") + matcher?.let { + val matchedAlias = (it as ServerSNIMatcher).matchedAlias + if (aliases.contains(matchedAlias)) { + logDebugWithMDC { "Found match for $matchedAlias." } + return matchedAlias + } + } + + logDebugWithMDC { "Unable to find a matching alias." } + return null + } + + private fun chooseClientAlias(keyStore: CertificateStore, clientLegalName: String?): String? { + clientLegalName?.let { + val aliases = keyStore.aliases() + if (aliases.isEmpty()) { + logDebugWithMDC { "Keystore doesn't contain any entries." } + } + aliases.forEach { alias -> + val x500Name = keyStore[alias].x509.subjectX500Principal + val aliasCordaX500Name = CordaX500Name.build(x500Name) + val clientCordaX500Name = CordaX500Name.parse(it) + if (clientCordaX500Name == aliasCordaX500Name) { + logDebugWithMDC { "Found alias $alias for $clientCordaX500Name." } + return alias + } + } + } + + return null + } + + private fun storeIfNotNull(func: () -> String?): String? { + val alias = func() + if (alias != null) { + lastAlias = alias + } + return alias + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 63128f3332..c6efef2e57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -1,28 +1,91 @@ package net.corda.nodeapi.internal.protonwrapper.netty +import io.netty.buffer.ByteBufAllocator +import io.netty.handler.ssl.ClientAuth +import io.netty.handler.ssl.SniHandler +import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslHandler +import io.netty.handler.ssl.SslProvider +import io.netty.util.DomainNameMappingBuilder import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHex import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.toBc +import net.corda.nodeapi.internal.crypto.x509 +import net.corda.nodeapi.internal.protonwrapper.netty.revocation.ExternalSourceRevocationChecker +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.DERIA5String +import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier +import org.bouncycastle.asn1.x509.CRLDistPoint +import org.bouncycastle.asn1.x509.DistributionPointName import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames import org.bouncycastle.asn1.x509.SubjectKeyIdentifier +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream import java.net.Socket +import java.security.KeyStore import java.security.cert.* import java.util.* +import java.util.concurrent.Executor import javax.net.ssl.* +import kotlin.system.measureTimeMillis private const val HOSTNAME_FORMAT = "%s.corda.net" -private const val SSL_HANDSHAKE_TIMEOUT_PROP_NAME = "corda.netty.sslHelper.handshakeTimeout" -private const val DEFAULT_SSL_TIMEOUT = 20000 // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT +internal const val DEFAULT = "default" -internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() { +internal const val DP_DEFAULT_ANSWER = "NO CRLDP ext" + +internal val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper") + +fun X509Certificate.distributionPoints() : Set? { + logger.debug("Checking CRLDPs for $subjectX500Principal") + + val crldpExtBytes = getExtensionValue(Extension.cRLDistributionPoints.id) + if (crldpExtBytes == null) { + logger.debug(DP_DEFAULT_ANSWER) + return emptySet() + } + + val derObjCrlDP = ASN1InputStream(ByteArrayInputStream(crldpExtBytes)).readObject() + val dosCrlDP = derObjCrlDP as? DEROctetString + if (dosCrlDP == null) { + logger.error("Expected to have DEROctetString, actual type: ${derObjCrlDP.javaClass}") + return emptySet() + } + val crldpExtOctetsBytes = dosCrlDP.octets + val dpObj = ASN1InputStream(ByteArrayInputStream(crldpExtOctetsBytes)).readObject() + val distPoint = CRLDistPoint.getInstance(dpObj) + if (distPoint == null) { + logger.error("Could not instantiate CRLDistPoint, from: $dpObj") + return emptySet() + } + + val dpNames = distPoint.distributionPoints.mapNotNull { it.distributionPoint }.filter { it.type == DistributionPointName.FULL_NAME } + val generalNames = dpNames.flatMap { GeneralNames.getInstance(it.name).names.asList() } + return generalNames.filter { it.tagNo == GeneralName.uniformResourceIdentifier}.map { DERIA5String.getInstance(it.name).string }.toSet() +} + +fun X509Certificate.distributionPointsToString() : String { + return with(distributionPoints()) { + if(this == null || isEmpty()) { + DP_DEFAULT_ANSWER + } else { + sorted().joinToString() + } + } +} + +@VisibleForTesting +class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() { companion object { val log = contextLogger() } @@ -45,12 +108,11 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) } catch (ex: Exception) { "null" } - " $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier]" + " $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] [${it.distributionPointsToString()}]" } return certs.joinToString("\r\n") } - private fun certPathToStringFull(chain: Array?): String { if (chain == null) { return "" @@ -107,6 +169,33 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) } +private object LoggingImmediateExecutor : Executor { + + override fun execute(command: Runnable?) { + val log = LoggerFactory.getLogger(javaClass) + + if (command == null) { + log.error("SSL handler executor called with a null command") + throw NullPointerException("command") + } + + @Suppress("TooGenericExceptionCaught", "MagicNumber") // log and rethrow all exceptions + try { + val commandName = command::class.qualifiedName?.let { "[$it]" } ?: "" + log.debug("Entering SSL command $commandName") + val elapsedTime = measureTimeMillis { command.run() } + log.debug("Exiting SSL command $elapsedTime millis") + if (elapsedTime > 100) { + log.info("Command: $commandName took $elapsedTime millis to execute") + } + } + catch (ex: Exception) { + log.error("Caught exception in SSL handler executor", ex) + throw ex + } + } +} + internal fun createClientSslHelper(target: NetworkHostAndPort, expectedRemoteLegalNames: Set, keyManagerFactory: KeyManagerFactory, @@ -125,13 +214,31 @@ internal fun createClientSslHelper(target: NetworkHostAndPort, sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single()))) sslEngine.sslParameters = sslParameters } - val sslHandler = SslHandler(sslEngine) - sslHandler.handshakeTimeoutMillis = Integer.getInteger(SSL_HANDSHAKE_TIMEOUT_PROP_NAME, DEFAULT_SSL_TIMEOUT).toLong() - return sslHandler + @Suppress("DEPRECATION") + return SslHandler(sslEngine, false, LoggingImmediateExecutor) } -internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, - trustManagerFactory: TrustManagerFactory): SslHandler { +internal fun createClientOpenSslHandler(target: NetworkHostAndPort, + expectedRemoteLegalNames: Set, + keyManagerFactory: KeyManagerFactory, + trustManagerFactory: TrustManagerFactory, + alloc: ByteBufAllocator): SslHandler { + val sslContext = SslContextBuilder.forClient().sslProvider(SslProvider.OPENSSL).keyManager(keyManagerFactory).trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory)).build() + val sslEngine = sslContext.newEngine(alloc, target.host, target.port) + sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + if (expectedRemoteLegalNames.size == 1) { + val sslParameters = sslEngine.sslParameters + sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single()))) + sslEngine.sslParameters = sslParameters + } + @Suppress("DEPRECATION") + return SslHandler(sslEngine, false, LoggingImmediateExecutor) +} + +internal fun createServerSslHandler(keyStore: CertificateStore, + keyManagerFactory: KeyManagerFactory, + trustManagerFactory: TrustManagerFactory): SslHandler { val sslContext = SSLContext.getInstance("TLS") val keyManagers = keyManagerFactory.keyManagers val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray() @@ -142,35 +249,106 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true - val sslHandler = SslHandler(sslEngine) - sslHandler.handshakeTimeoutMillis = Integer.getInteger(SSL_HANDSHAKE_TIMEOUT_PROP_NAME, DEFAULT_SSL_TIMEOUT).toLong() - return sslHandler + val sslParameters = sslEngine.sslParameters + sslParameters.sniMatchers = listOf(ServerSNIMatcher(keyStore)) + sslEngine.sslParameters = sslParameters + @Suppress("DEPRECATION") + return SslHandler(sslEngine, false, LoggingImmediateExecutor) } -internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { - val certPathBuilder = CertPathBuilder.getInstance("PKIX") - val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker - revocationChecker.options = EnumSet.of( - // Prefer CRL over OCSP - PKIXRevocationChecker.Option.PREFER_CRLS, - // Don't fall back to OCSP checking - PKIXRevocationChecker.Option.NO_FALLBACK) - if (crlCheckSoftFail) { - // Allow revocation check to succeed if the revocation status cannot be determined for one of - // the following reasons: The CRL or OCSP response cannot be obtained because of a network error. - revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL - } +@VisibleForTesting +fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters { val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector()) + val revocationChecker = when (revocationConfig.mode) { + RevocationConfig.Mode.OFF -> AllowAllRevocationChecker // Custom PKIXRevocationChecker skipping CRL check + RevocationConfig.Mode.EXTERNAL_SOURCE -> { + require(revocationConfig.externalCrlSource != null) { "externalCrlSource must not be null" } + ExternalSourceRevocationChecker(revocationConfig.externalCrlSource!!) { Date() } // Custom PKIXRevocationChecker which uses `externalCrlSource` + } + else -> { + val certPathBuilder = CertPathBuilder.getInstance("PKIX") + val pkixRevocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker + pkixRevocationChecker.options = EnumSet.of( + // Prefer CRL over OCSP + PKIXRevocationChecker.Option.PREFER_CRLS, + // Don't fall back to OCSP checking + PKIXRevocationChecker.Option.NO_FALLBACK) + if (revocationConfig.mode == RevocationConfig.Mode.SOFT_FAIL) { + // Allow revocation check to succeed if the revocation status cannot be determined for one of + // the following reasons: The CRL or OCSP response cannot be obtained because of a network error. + pkixRevocationChecker.options = pkixRevocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL + } + pkixRevocationChecker + } + } pkixParams.addCertPathChecker(revocationChecker) return CertPathTrustManagerParameters(pkixParams) } +internal fun createServerOpenSslHandler(keyManagerFactory: KeyManagerFactory, + trustManagerFactory: TrustManagerFactory, + alloc: ByteBufAllocator): SslHandler { + + val sslContext = getServerSslContextBuilder(keyManagerFactory, trustManagerFactory).build() + val sslEngine = sslContext.newEngine(alloc) + sslEngine.useClientMode = false + @Suppress("DEPRECATION") + return SslHandler(sslEngine, false, LoggingImmediateExecutor) +} + +/** + * Creates a special SNI handler used only when openSSL is used for AMQPServer + */ +internal fun createServerSNIOpenSslHandler(keyManagerFactoriesMap: Map, + trustManagerFactory: TrustManagerFactory): SniHandler { + + // Default value can be any in the map. + val sslCtxBuilder = getServerSslContextBuilder(keyManagerFactoriesMap.values.first(), trustManagerFactory) + val mapping = DomainNameMappingBuilder(sslCtxBuilder.build()) + keyManagerFactoriesMap.forEach { + mapping.add(it.key, sslCtxBuilder.keyManager(it.value).build()) + } + return SniHandler(mapping.build()) +} + +@Suppress("SpreadOperator") +private fun getServerSslContextBuilder(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslContextBuilder { + return SslContextBuilder.forServer(keyManagerFactory) + .sslProvider(SslProvider.OPENSSL) + .trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory)) + .clientAuth(ClientAuth.REQUIRE) + .ciphers(ArtemisTcpTransport.CIPHER_SUITES) + .protocols(*ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()) +} + +internal fun splitKeystore(config: AMQPConfiguration): Map { + val keyStore = config.keyStore.value.internal + val password = config.keyStore.entryPassword.toCharArray() + return keyStore.aliases().toList().map { alias -> + val key = keyStore.getKey(alias, password) + val certs = keyStore.getCertificateChain(alias) + val x500Name = keyStore.getCertificate(alias).x509.subjectX500Principal + val cordaX500Name = CordaX500Name.build(x500Name) + val newKeyStore = KeyStore.getInstance("JKS") + newKeyStore.load(null) + newKeyStore.setKeyEntry(alias, key, password, certs) + val newKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + newKeyManagerFactory.init(newKeyStore, password) + x500toHostName(cordaX500Name) to CertHoldingKeyManagerFactoryWrapper(newKeyManagerFactory, config) + }.toMap() +} + // As per Javadoc in: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/KeyManagerFactory.html `init` method // 2nd parameter `password` - the password for recovering keys in the KeyStore fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.entryPassword.toCharArray()) fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal) +/** + * Method that converts a [CordaX500Name] to a a valid hostname (RFC-1035). It's used for SNI to indicate the target + * when trying to communicate with nodes that reside behind the same firewall. This is a solution to TLS's extension not + * yet supporting x500 names as server names + */ internal fun x500toHostName(x500Name: CordaX500Name): String { val secureHash = SecureHash.sha256(x500Name.toString()) // RFC 1035 specifies a limit 255 bytes for hostnames with each label being 63 bytes or less. Due to this, the string diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ServerSNIMatcher.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ServerSNIMatcher.kt new file mode 100644 index 0000000000..7cff6bf55d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/ServerSNIMatcher.kt @@ -0,0 +1,47 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.x509 +import javax.net.ssl.SNIHostName +import javax.net.ssl.SNIMatcher +import javax.net.ssl.SNIServerName +import javax.net.ssl.StandardConstants + +class ServerSNIMatcher(private val keyStore: CertificateStore) : SNIMatcher(0) { + + companion object { + val log = contextLogger() + } + + var matchedAlias: String? = null + private set + var matchedServerName: String? = null + private set + + override fun matches(serverName: SNIServerName): Boolean { + if (serverName.type == StandardConstants.SNI_HOST_NAME) { + keyStore.aliases().forEach { alias -> + val x500Name = keyStore[alias].x509.subjectX500Principal + val cordaX500Name = CordaX500Name.build(x500Name) + // Convert the CordaX500Name into the expected host name and compare + // E.g. O=Corda B, L=London, C=GB becomes 3c6dd991936308edb210555103ffc1bb.corda.net + if ((serverName as SNIHostName).asciiName == x500toHostName(cordaX500Name)) { + matchedAlias = alias + matchedServerName = serverName.asciiName + return true + } + } + } + + val knownSNIValues = keyStore.aliases().joinToString { + val x500Name = keyStore[it].x509.subjectX500Principal + val cordaX500Name = CordaX500Name.build(x500Name) + "hostname = ${x500toHostName(cordaX500Name)} alias = $it" + } + val requestedSNIValue = "hostname = ${(serverName as SNIHostName).asciiName}" + log.warn("The requested SNI value [$requestedSNIValue] does not match any of the following known SNI values [$knownSNIValues]") + return false + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/TrustManagerFactoryWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/TrustManagerFactoryWrapper.kt new file mode 100644 index 0000000000..7565b1cdc2 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/TrustManagerFactoryWrapper.kt @@ -0,0 +1,40 @@ +package net.corda.nodeapi.internal.protonwrapper.netty + +import java.security.KeyStore +import javax.net.ssl.ManagerFactoryParameters +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.TrustManagerFactorySpi +import javax.net.ssl.X509ExtendedTrustManager + +class LoggingTrustManagerFactorySpiWrapper(private val factorySpi: TrustManagerFactorySpi) : TrustManagerFactorySpi() { + override fun engineGetTrustManagers(): Array { + val engineGetTrustManagersMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineGetTrustManagers") + engineGetTrustManagersMethod.isAccessible = true + @Suppress("UNCHECKED_CAST") + val trustManagers = engineGetTrustManagersMethod.invoke(factorySpi) as Array + return if (factorySpi is LoggingTrustManagerFactorySpiWrapper) trustManagers else trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray() + } + + override fun engineInit(ks: KeyStore?) { + val engineInitMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineInit", KeyStore::class.java) + engineInitMethod.isAccessible = true + engineInitMethod.invoke(factorySpi, ks) + } + + override fun engineInit(spec: ManagerFactoryParameters?) { + val engineInitMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineInit", ManagerFactoryParameters::class.java) + engineInitMethod.isAccessible = true + engineInitMethod.invoke(factorySpi, spec) + } +} + +class LoggingTrustManagerFactoryWrapper(factory: TrustManagerFactory) : TrustManagerFactory(getFactorySpi(factory), factory.provider, factory.algorithm) { + companion object { + private fun getFactorySpi(factory: TrustManagerFactory): TrustManagerFactorySpi { + val spiField = TrustManagerFactory::class.java.getDeclaredField("factorySpi") + spiField.isAccessible = true + return LoggingTrustManagerFactorySpiWrapper(spiField.get(factory) as TrustManagerFactorySpi) + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/revocation/ExternalSourceRevocationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/revocation/ExternalSourceRevocationChecker.kt new file mode 100644 index 0000000000..23af94ca3d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/revocation/ExternalSourceRevocationChecker.kt @@ -0,0 +1,88 @@ +package net.corda.nodeapi.internal.protonwrapper.netty.revocation + +import net.corda.core.utilities.contextLogger +import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource +import org.bouncycastle.asn1.x509.Extension +import java.security.cert.CRLReason +import java.security.cert.CertPathValidatorException +import java.security.cert.Certificate +import java.security.cert.CertificateRevokedException +import java.security.cert.PKIXRevocationChecker +import java.security.cert.X509CRL +import java.security.cert.X509Certificate +import java.util.* + +/** + * Implementation of [PKIXRevocationChecker] which determines whether certificate is revoked using [externalCrlSource] which knows how to + * obtain a set of CRLs for a given certificate from an external source + */ +class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrlSource, private val dateSource: () -> Date) : PKIXRevocationChecker() { + + companion object { + private val logger = contextLogger() + } + + override fun check(cert: Certificate, unresolvedCritExts: MutableCollection?) { + val x509Certificate = cert as X509Certificate + checkApprovedCRLs(x509Certificate, externalCrlSource.fetch(x509Certificate)) + } + + /** + * Borrowed from `RevocationChecker.checkApprovedCRLs()` + */ + @Suppress("NestedBlockDepth") + @Throws(CertPathValidatorException::class) + private fun checkApprovedCRLs(cert: X509Certificate, approvedCRLs: Set) { + // See if the cert is in the set of approved crls. + logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() cert SN: ${cert.serialNumber}") + + for (crl in approvedCRLs) { + val entry = crl.getRevokedCertificate(cert) + if (entry != null) { + logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() CRL entry: $entry") + + /* + * Abort CRL validation and throw exception if there are any + * unrecognized critical CRL entry extensions (see section + * 5.3 of RFC 5280). + */ + val unresCritExts = entry.criticalExtensionOIDs + if (unresCritExts != null && !unresCritExts.isEmpty()) { + /* remove any that we will process */ + unresCritExts.remove(Extension.cRLDistributionPoints.id) + unresCritExts.remove(Extension.certificateIssuer.id) + if (!unresCritExts.isEmpty()) { + throw CertPathValidatorException( + "Unrecognized critical extension(s) in revoked CRL entry: $unresCritExts") + } + } + + val reasonCode = entry.revocationReason ?: CRLReason.UNSPECIFIED + val revocationDate = entry.revocationDate + if (revocationDate.before(dateSource())) { + val t = CertificateRevokedException( + revocationDate, reasonCode, + crl.issuerX500Principal, mutableMapOf()) + throw CertPathValidatorException( + t.message, t, null, -1, CertPathValidatorException.BasicReason.REVOKED) + } + } + } + } + + override fun isForwardCheckingSupported(): Boolean { + return true + } + + override fun getSupportedExtensions(): MutableSet? { + return null + } + + override fun init(forward: Boolean) { + // Nothing to do + } + + override fun getSoftFailExceptions(): MutableList { + return LinkedList() + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt index ef6bec0348..9c15c0f478 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt @@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import org.assertj.core.api.Assertions.* -import org.hibernate.exception.DataException import org.junit.Test import java.net.URL import java.nio.file.Path diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt index 3802a357b0..782d8b8abe 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelperTest.kt @@ -4,7 +4,10 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.coretesting.internal.configureTestSSL +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS +import net.corda.nodeapi.internal.DEV_CA_PRIVATE_KEY_PASS import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import org.junit.Test import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SNIHostName @@ -23,7 +26,7 @@ class SSLHelperTest { val keyStore = sslConfig.keyStore keyManagerFactory.init(CertificateStore.fromFile(keyStore.path, keyStore.storePassword, keyStore.entryPassword, false)) val trustStore = sslConfig.trustStore - trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(trustStore.path, trustStore.storePassword, trustStore.entryPassword, false), false)) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(trustStore.path, trustStore.storePassword, trustStore.entryPassword, false), RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL))) val sslHandler = createClientSslHelper(NetworkHostAndPort("localhost", 1234), setOf(legalName), keyManagerFactory, trustManagerFactory) val legalNameHash = SecureHash.sha256(legalName.toString()).toString().take(32).toLowerCase() @@ -34,4 +37,14 @@ class SSLHelperTest { assertEquals(1, sslHandler.engine().sslParameters.serverNames.size) assertEquals("$legalNameHash.corda.net", (sslHandler.engine().sslParameters.serverNames.first() as SNIHostName).asciiName) } + + @Test(timeout=300_000) + fun `test distributionPointsToString`() { + val certStore = CertificateStore.fromResource( + "net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks", + DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS) + val distPoints = certStore.query { getCertificateChain(CORDA_CLIENT_TLS).map { it.distributionPointsToString() } } + assertEquals(listOf("NO CRLDP ext", "http://day-v3-doorman.cordaconnect.io/doorman", + "http://day3-doorman.cordaconnect.io/doorman", "http://day3-doorman.cordaconnect.io/subordinate", "NO CRLDP ext"), distPoints) + } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/revocation/ExternalSourceRevocationCheckerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/revocation/ExternalSourceRevocationCheckerTest.kt new file mode 100644 index 0000000000..7be350a525 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/revocation/ExternalSourceRevocationCheckerTest.kt @@ -0,0 +1,56 @@ +package net.corda.nodeapi.internal.protonwrapper.netty.revocation + +import net.corda.core.utilities.Try +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS +import net.corda.nodeapi.internal.DEV_CA_PRIVATE_KEY_PASS +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource +import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory +import org.junit.Test +import java.math.BigInteger + +import java.security.cert.X509CRL +import java.security.cert.X509Certificate +import java.sql.Date +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ExternalSourceRevocationCheckerTest { + + @Test(timeout=300_000) + fun checkRevoked() { + val checkResult = performCheckOnDate(Date.valueOf("2019-09-27")) + val failedChecks = checkResult.filterNot { it.second.isSuccess } + assertEquals(1, failedChecks.size) + assertEquals(BigInteger.valueOf(8310484079152632582), failedChecks.first().first.serialNumber) + } + + @Test(timeout=300_000) + fun checkTooEarly() { + val checkResult = performCheckOnDate(Date.valueOf("2019-08-27")) + assertTrue(checkResult.all { it.second.isSuccess }) + } + + private fun performCheckOnDate(date: Date): List>> { + val certStore = CertificateStore.fromResource( + "net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks", + DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS) + + val resourceAsStream = javaClass.getResourceAsStream("/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl") + val crl = CertificateFactory().engineGenerateCRL(resourceAsStream) as X509CRL + + //val crlHolder = X509CRLHolder(resourceAsStream) + //crlHolder.revokedCertificates as X509CRLEntryHolder + + val instance = ExternalSourceRevocationChecker(object : ExternalCrlSource { + override fun fetch(certificate: X509Certificate): Set = setOf(crl) + }) { date } + + return certStore.query { + getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).map { + Pair(it, Try.on { instance.check(it, mutableListOf()) }) + } + } + } +} \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/Readme.txt b/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/Readme.txt new file mode 100644 index 0000000000..e312c76d10 --- /dev/null +++ b/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/Readme.txt @@ -0,0 +1,3 @@ +Represents some test data which contains real certificates produced by DayWatch Doorman as well as CRL list file. + +For all the keystores the password is "cordacadevpass". \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl b/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl new file mode 100644 index 0000000000000000000000000000000000000000..469901e9aa88897242fedba42004e35b1af45254 GIT binary patch literal 576 zcmXqLVzM!4V!Xk`$Y>zK#-Y{ban6>7nF-2bW@I-=GvqelWMd9xVH0Kw4K@@s;0JNo zg*p6E%M~K?i?R*HfI5LfT*ACT#tI(!IVsNh3O+v0hI|G*AaQPCR_FYplte=r11S)n zSyRi)WT4K(;dgb_ ztkD1aSRs-IW`@QF#>R$G20R8_K!asPS(v#PIYA1GLyp>7=1pdUDzr2-2P!m1Rmi%a z@sUB}eS^l?Aic^gjgt);Cp3s?NXl6`iX8CwTm0o^weE_Ap}sZl3-gxb7-Uf`925-X zAmJd691g{$Nx)Fb%u6guZQ@Wj@V$0<*5Vlk_8`Z}Ff#sUF*Pt=V7NfPO*f;Yq`*pF zKP9nJx6D`<8hm=mpde1p&&x|qF44=(*N2K@k5pz41_M_nh4AL&sI{Ay+UM8LU;l#V z{957jhc1;ZowfKvpZRkK`HUw_iVUxd|CGOdTYEm$FM+W?CP09TW$VYohLhs<2HEL0 Id_O1$06rO?9smFU literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks b/node-api/src/test/resources/net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks new file mode 100644 index 0000000000000000000000000000000000000000..67e111462433d10035b4a7bc402f9275ba70efbc GIT binary patch literal 3594 zcmd7T3sh2h9tUvl<${u-F*s;V8s->22K*!WARnP(Scx?mrRFpPYU-e1h&idd3LMGG zvCLUVn=%U~9c_A-r!=+PqIM^(jBI*nZjaI9SZ4Ozw7n=DX|mawbKK5y_#f_{%YFU+ zpWm0^zTrL$1_Pr58X1&pKduF@uG7lwW8h zYX}=#@kfg|K9ZM>r`GxH;E5RCyAKBFKCb1}H4%xk7c#CisaP$F&t~pUZ;grH81(d4 zhqFgBZQ?@iD?FFn=x%hu;7`~0WJ32=kb1792=fjz%(G8g9{OdmZ~8wrELfMneslXp zi#&nnsui|fE}J;b&YSq`oUQ}&_4}T6v~GKPT}yY>J~?_~pnN}BhmF}=k3JU;gNtN% z0ybI&H=u-RKnXQhU||S`&AFX@-uH@@Uk28Ab60g}i$8*ejayjA3I{1SpDu9r~zt zmIwv0hFXdumOk)k>bYGYTcf?bz1426JD65W*$uC zC{i*PmwxwZfrb{LI{##gS6b}q4{F8E0a`qk9yf4fOwG@z8a+C)jcU@Cp7v);it%95 zm>RIk)Jg=C1gs1=%*f{9@fp-8sZ7kIpm%KWLMAR@QA4H4aluk*SXeOAa-@2Q1sNA? zXicV}Qt0onWHMK{cDYa1fu`foVXI5Y{UNBf|R@9 zIjEh+2VNnEc#WD9y3cA0=J6l-O|hu13!l%UK3tHyj(orh?Bg`lgcPg1LlWVx!ARnM zjS})i8<1%NYTWfURBsd%4q#DNCZ-_uA2StYIEB5zOP0Kx}9{$?;tnbsT*zoY))##r%O3lh3-;k8* z(F-^sh5&jUye1L+HdY7(8qPpP<^Uhy_4*hX0NN3{b~ zdj(zXHs<}$X*Wjt*7H^QhaLYeDlAVN3fr5QY%9))@~FdlJZ7xSzoylaVCqU52Xcf~ zH=c7Y;74_QzfxYApskrjdbxPXV8JBw|FQL?1&>=T+5Adq5YiG|s_6GOZ3FAH$ z*o<7!9;pyrJr}HzzC^y9V9Yy+| zwv;tA?Yh(-9Cef{Bc`?gu+`CHsZ>E{dLDbAJ{j+l6kK*L4ac-dR(7x ziT-Ukn_@8julL5xn4WV}R~XGaez#vYt1Q&4hx0RT*5&faA`+>&5~fn_cdZ|2n48KN v(C$$LM0dcF9IbgN{%ejM_Yv)zwxTK#_d@Db+V#*6cO5Eam7J;f>E8Pv6ki8X literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/networkParamsWrite b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/networkParamsWrite new file mode 100644 index 0000000000000000000000000000000000000000..dcdbaa7b5f0d92f240af49f1068fcaee9c5b77f2 GIT binary patch literal 3066 zcmcImTWl0n7@ob%P7#CHh$xCF4XB|yy_YJbOt(99mtH7zw`{j2=Ire0_O!ESmYK8N zZh0|E@bYAg#>51}u@Al?FG5Vr7}RJ&q{f&iQ4+(8C{bffG=Zr9Y;`-kI)#StGM(=E z=D&Ua_y0#T%nb4gg7C~oX?O~PZ%Gi=Uxt@}u-Hg3lQjqNfa7^fqH= zSKK_1-LnVK(se-ltI#vFYdWv0X5T_vegMyQV5`u*|KKvZ-f?mYK3i9|TwkQQ zbDb0OXl&k^=uh^yCJr4?2ZoxLefwg74#lfujppZKLlZND(PYQ%2-aYjV1+yQDj~hLEE04I>6aW~pHen{!0NJd`S!M4MK)zdtH0Zd)n! z44Ol-g*7;4C3&=v)`>QVOD7*|+1Vn70!=s{*e0C+>yJa%qd%Wl>pC{E_tIZZUYaa) z`zNn$FzPOTcjgNzd;}Q$zWfjtAE+_TNDj*}YB5BaE&3BeT!{NB9pDE}#it#XoR@KU z5i4gR{N#Jb`H6Q_jLOu=V3a2^p$wZ8=`tJ@4Rcl=S^hhUOq9cLW+`gLRKR}N@9r=! zmKDckuS0X?)1bXW;`inVCAmUwR7Y&eFmvNzo*0w|APSZHT9m%9>w~z&>!?Ej!4~)J zv=txUQ!aFY5nUc47VEj0e99EY=#5c%V4D!A+GT8CabR&3(0L>aW3p#?|=QwFl)TUVUJ7{c~p;<1a4MWF>s$85xx`o}XAljdcH?Y1+Dqf>Tv5&e_X_t7Af~5EkuQd9d zsh#X>X&C`nZ8}hibw-gwDlk_ZHEi+m%^qW?s=dnckCf$qbDmd8)oWTR_GP!yw?OGN zBbPI%lL-%5%!Ifhqp(}v4HDmIm{~-L1r!-HK?(E1J_w?6V_mqak#QgNi*9H@JM_zn z-zv-|P#cU<<{8*#iA$PMpv?5JvF-!dfbKb%jx{G^bpPl^;&^4$q!_y>wyk z@6VyDXDWF6Lej}HM?U~lGYd@7^aTXV0 zu+C&Bi!pqpfT@OiD>bj3l3sT*#co{L{XyI5=(JN79!e7m7na(jPK&n`-|b5zu2tMEkn9R#I~VKfKPqv5dQD^chAcWm=Wk|j B^OXPq literal 0 HcmV?d00001 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 221d11cd9a..be935b0856 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 @@ -22,6 +22,7 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString @@ -206,13 +207,22 @@ class AMQPBridgeTest { val artemisClient = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) artemisServer.start() artemisClient.start() - val bridgeManager = AMQPBridgeManager(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE, artemisConfig.crlCheckSoftFail) + val bridgeManager = AMQPBridgeManager( + artemisConfig.p2pSslOptions.keyStore.get(), + artemisConfig.p2pSslOptions.trustStore.get(), + false, + null, + MAX_MESSAGE_SIZE, + artemisConfig.crlCheckSoftFail.toRevocationConfig(), + false, { ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) }, trace = false, + sslHandshakeTimeout = null, + bridgeConnectionTTLSeconds = 0) bridgeManager.start() val artemis = artemisClient.started!! if (sourceQueueName != null) { // Local queue for outgoing messages artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) - bridgeManager.deployBridge(sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) + bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) } return Triple(artemisServer, artemisClient, bridgeManager) } @@ -228,7 +238,6 @@ class AMQPBridgeTest { doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions - doReturn(true).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() @@ -238,7 +247,6 @@ class AMQPBridgeTest { override val trustStore = serverConfig.p2pSslOptions.trustStore.get() override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize - override val crlCheckSoftFail: Boolean = serverConfig.crlCheckSoftFail } return AMQPServer("0.0.0.0", amqpAddress.port, diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt index 1ab13fc6d7..6b884ef300 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt @@ -29,6 +29,8 @@ import net.corda.coretesting.internal.DEV_INTERMEDIATE_CA import net.corda.coretesting.internal.DEV_ROOT_CA import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig +import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.* @@ -305,7 +307,7 @@ class CertificateRevocationListNodeTests { } @Test(timeout=300_000) - fun `Revocation status chceck fails when the CRL distribution point is not set and soft fail is disabled`() { + fun `Revocation status check fails when the CRL distribution point is not set and soft fail is disabled`() { val crlCheckSoftFail = false val (amqpServer, _) = createServer( serverPort, @@ -380,7 +382,6 @@ class CertificateRevocationListNodeTests { val amqpConfig = object : AMQPConfiguration { override val keyStore = keyStore override val trustStore = clientConfig.p2pSslOptions.trustStore.get() - override val crlCheckSoftFail: Boolean = crlCheckSoftFail override val maxMessageSize: Int = maxMessageSize } return Pair(AMQPClient( @@ -404,7 +405,6 @@ class CertificateRevocationListNodeTests { doReturn(name).whenever(it).myLegalName doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(signingCertificateStore).whenever(it).signingCertificateStore - doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) @@ -412,7 +412,7 @@ class CertificateRevocationListNodeTests { val amqpConfig = object : AMQPConfiguration { override val keyStore = keyStore override val trustStore = serverConfig.p2pSslOptions.trustStore.get() - override val crlCheckSoftFail: Boolean = crlCheckSoftFail + override val revocationConfig = crlCheckSoftFail.toRevocationConfig() override val maxMessageSize: Int = maxMessageSize } return Pair(AMQPServer( diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index d3515e56bd..874d1feca4 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -32,6 +32,7 @@ import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig import org.apache.activemq.artemis.api.core.RoutingType import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertArrayEquals @@ -39,6 +40,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit import javax.net.ssl.* import kotlin.concurrent.thread import kotlin.test.assertEquals @@ -341,6 +343,7 @@ class ProtonWrapperTests { val connection1ID = CordaX500Name.build(connection1.remoteCert!!.subjectX500Principal) assertEquals("client 0", connection1ID.organisationUnit) val source1 = connection1.remoteAddress + val client2Connected = amqpClient2.onConnection.toFuture() amqpClient2.start() val connection2 = connectionEvents.next() assertEquals(true, connection2.connected) @@ -353,6 +356,7 @@ class ProtonWrapperTests { assertEquals(false, connection3.connected) assertEquals(source1, connection3.remoteAddress) assertEquals(false, amqpClient1.connected) + client2Connected.get(60, TimeUnit.SECONDS) assertEquals(true, amqpClient2.connected) // Now shutdown both amqpClient2.stop() @@ -362,11 +366,13 @@ class ProtonWrapperTests { assertEquals(false, amqpClient1.connected) assertEquals(false, amqpClient2.connected) // Now restarting one should work + val client1Connected = amqpClient1.onConnection.toFuture() amqpClient1.start() val connection5 = connectionEvents.next() assertEquals(true, connection5.connected) val connection5ID = CordaX500Name.build(connection5.remoteCert!!.subjectX500Principal) assertEquals("client 0", connection5ID.organisationUnit) + client1Connected.get(60, TimeUnit.SECONDS) assertEquals(true, amqpClient1.connected) assertEquals(false, amqpClient2.connected) // Cleanup @@ -447,7 +453,6 @@ class ProtonWrapperTests { override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize - override val crlCheckSoftFail: Boolean = clientConfig.crlCheckSoftFail } return AMQPClient( listOf(NetworkHostAndPort("localhost", serverPort), @@ -479,7 +484,6 @@ class ProtonWrapperTests { override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize - override val crlCheckSoftFail: Boolean = clientConfig.crlCheckSoftFail } return AMQPClient( listOf(NetworkHostAndPort("localhost", serverPort)), @@ -502,7 +506,6 @@ class ProtonWrapperTests { doReturn(name).whenever(it).myLegalName doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions - doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() @@ -512,8 +515,8 @@ class ProtonWrapperTests { override val keyStore = serverKeystore override val trustStore = serverTruststore override val trace: Boolean = true + override val revocationConfig = crlCheckSoftFail.toRevocationConfig() override val maxMessageSize: Int = maxMessageSize - override val crlCheckSoftFail: Boolean = serverConfig.crlCheckSoftFail } return AMQPServer( "0.0.0.0", 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 eee9e063f0..951fb25988 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -79,6 +79,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig import net.corda.serialization.internal.AMQP_P2P_CONTEXT import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.serialization.internal.AMQP_RPC_SERVER_CONTEXT @@ -417,7 +418,15 @@ open class Node(configuration: NodeConfiguration, failoverCallback = { errorAndTerminate("ArtemisMessagingClient failed. Shutting down.", null) } ) } - return BridgeControlListener(configuration.p2pSslOptions, networkParameters.maxMessageSize, configuration.crlCheckSoftFail, artemisMessagingClientFactory) + return BridgeControlListener( + configuration.p2pSslOptions.keyStore.get(), + configuration.p2pSslOptions.trustStore.get(), + false, + null, + networkParameters.maxMessageSize, + configuration.crlCheckSoftFail.toRevocationConfig(), + false, + artemisMessagingClientFactory) } private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses? { 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 b4b74bc505..8254c0138f 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 @@ -197,7 +197,7 @@ class P2PMessagingClient(val config: NodeConfiguration, inboxes += RemoteInboxAddress(it).queueName } - inboxes.forEach { createQueueIfAbsent(it, producerSession!!, exclusive = true) } + inboxes.forEach { createQueueIfAbsent(it, producerSession!!, exclusive = true, isServiceAddress = false) } p2pConsumer = P2PMessagingConsumer(inboxes, createNewSession, isDrainingModeOn, drainingModeWasChangedEvents, metricRegistry) @@ -267,7 +267,7 @@ class P2PMessagingClient(val config: NodeConfiguration, return state.locked { node.legalIdentitiesAndCerts.map { val messagingAddress = NodeAddress(it.party.owningKey) - BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }) + BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false) }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() } } @@ -306,7 +306,7 @@ class P2PMessagingClient(val config: NodeConfiguration, val keyHash = queueName.substring(PEERS_PREFIX.length) val peers = networkMap.getNodesByOwningKeyIndex(keyHash) for (node in peers) { - val bridge = BridgeEntry(queueName.toString(), node.addresses, node.legalIdentities.map { it.name }) + val bridge = BridgeEntry(queueName.toString(), node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false) requiredBridges += bridge knownQueues += queueName.toString() } @@ -527,19 +527,20 @@ class P2PMessagingClient(val config: NodeConfiguration, val internalTargetQueue = (address as? ArtemisAddress)?.queueName ?: throw IllegalArgumentException("Not an Artemis address") state.locked { - createQueueIfAbsent(internalTargetQueue, producerSession!!, exclusive = address !is ServiceAddress) + val serviceAddress = address is ServiceAddress + createQueueIfAbsent(internalTargetQueue, producerSession!!, exclusive = !serviceAddress, isServiceAddress = serviceAddress) } internalTargetQueue } } /** Attempts to create a durable queue on the broker which is bound to an address of the same name. */ - private fun createQueueIfAbsent(queueName: String, session: ClientSession, exclusive: Boolean) { + private fun createQueueIfAbsent(queueName: String, session: ClientSession, exclusive: Boolean, isServiceAddress: Boolean) { fun sendBridgeCreateMessage() { val keyHash = queueName.substring(PEERS_PREFIX.length) val peers = networkMap.getNodesByOwningKeyIndex(keyHash) for (node in peers) { - val bridge = BridgeEntry(queueName, node.addresses, node.legalIdentities.map { it.name }) + val bridge = BridgeEntry(queueName, node.addresses, node.legalIdentities.map { it.name }, isServiceAddress) val createBridgeMessage = BridgeControl.Create(config.myLegalName.toString(), bridge) sendBridgeControl(createBridgeMessage) } From 9495efc50ca6b3db95704ea52f3c48687aabd8e0 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 16 Mar 2020 12:27:55 +0000 Subject: [PATCH 12/22] Updates DJVM docs. (#6043) --- docs/source/key-concepts-djvm.rst | 77 ++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/docs/source/key-concepts-djvm.rst b/docs/source/key-concepts-djvm.rst index 649f24d573..bbd31a45d7 100644 --- a/docs/source/key-concepts-djvm.rst +++ b/docs/source/key-concepts-djvm.rst @@ -16,14 +16,6 @@ So, what does it mean for a piece of code to be fully deterministic? Ultimately as a function, is pure. In other words, given the same set of inputs, it will always produce the same set of outputs without inflicting any side-effects that might later affect the computation. -.. important:: The code in the DJVM module has not yet been integrated with the rest of the platform. It will eventually become a - part of the node and enforce deterministic and secure execution of smart contract code, which is mobile and may - propagate around the network without human intervention. - - Currently, it stands alone as an evaluation version. We want to give developers the ability to start trying it out and - get used to developing deterministic code under the set of constraints that we envision will be placed on contract code - in the future. - Non-Determinism ~~~~~~~~~~~~~~~ @@ -272,34 +264,73 @@ The DJVM doesn't support multi-threading and so synchronised methods and code bl use in sandboxed code. Consequently, we automatically transform them into ordinary methods and code blocks instead. -Future Work -~~~~~~~~~~~ +Trying out the DJVM +~~~~~~~~~~~~~~~~~~~ -Further work is planned: +.. warning:: The code in the DJVM module is still a beta release. It has been partially integrated with Corda to allow contract + verification. However, DJVM-enabled nodes cannot yet participate in a general Corda network containing nodes that do not use the DJVM. It + is provided to allow developers to try out the DJVM and experiment with developing deterministic code under the set of constraints that + we envision will be placed on contract code in the future. - * To enable controlled use of reflection APIs. +Tweaking Your Contract Code +........................... - * Currently, dynamic invocation is disallowed. Allow specific lambda and - string concatenation meta-factories used by Java code itself. +CorDapp developers may need to tweak their contract CorDapps for use inside the DJVM. This is because not every class, constructor or +method defined in the ``corda-core`` and ``corda-serialization`` modules is available when running inside the sandbox. - * Map more mathematical operations to use their 'exact' counterparts. +During development, you can choose to compile individual CorDapp modules against the DJVM by defining the following +``deterministic.gradle`` script plugin: - * General tightening of the enforced constraints. +.. code-block:: shell - * Cost accounting of runtime metrics such as memory allocation, branching and - exception handling. More specifically defining sensible runtime thresholds - and make further improvements to the instrumentation. + configurations { + compileClasspath { Configuration c -> deterministic(c) } + } - * More sophisticated runtime accounting as discussed in `Runtime Costing`_. + private final void deterministic(Configuration configuration) { + if (configuration.state == Configuration.State.UNRESOLVED) { + // Ensure that this module uses the deterministic Corda artifacts. + configuration.resolutionStrategy.dependencySubstitution { + substitute module("$corda_release_group:corda-serialization") with module("$corda_release_group:corda-serialization-deterministic:$corda_release_version") + substitute module("$corda_release_group:corda-core") with module("$corda_release_group:corda-core-deterministic:$corda_release_version") + } + } + } +And applying it to individual modules of your CorDapp using: -Command-line Tool -~~~~~~~~~~~~~~~~~ +.. code-block:: shell + + apply from: "${rootProject.projectDir}/deterministic.gradle" + +Uses of Corda's core or serialization APIs that are unavailable inside the sandbox will then cause compilation errors. + +Note however that successful compilation against ``corda-core-deterministic`` and ``corda-serialization-deterministic`` is +not sufficient. The only way to be sure that a piece of code is deterministic is to actually run it inside a DJVM sandbox, +as described below. + +Enabling Use of the DJVM for a Node +................................... + +You can enable the DJVM for your node by adding the following line to your node's ``node.conf`` file: + +.. code-block:: shell + + systemProperties = { "net.corda.djvm" = true } + +This will cause your node to sandbox every call to ``Contract.verify``. If your transaction contains a source of non-determinism, +transaction verification will fail. + +Alternatively, you can enable the DJVM when creating nodes via DemoBench by ticking the ``Deterministic Contract Verification`` checkbox +when creating the initial notary node. + +Using the Command-line Tool +........................... You can download and unpack ``corda-djvm-cli.zip`` from the R3 Artifactory. Alternatively, you can build it yourself from the source as follows. -Open your terminial and clone the DJVM repository from GitHub: +Open your terminal and clone the DJVM repository from GitHub: .. code-block:: shell From f476c1581d8d7a17b4bf43777786e330bfc01d4e Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 17 Mar 2020 08:48:45 +0000 Subject: [PATCH 13/22] CORDA-3377: Upgrade to DJVM 1.0. (#6071) --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 10bc33dcab..27a3493ced 100644 --- a/constants.properties +++ b/constants.properties @@ -30,7 +30,7 @@ snakeYamlVersion=1.19 caffeineVersion=2.7.0 metricsVersion=4.1.0 metricsNewRelicVersion=1.1.1 -djvmVersion=1.0-RC10 +djvmVersion=1.0 deterministicRtVersion=1.0-RC02 openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 From 861b76949975cc2e7c16a34f2ccf14bd81b5ff39 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Tue, 17 Mar 2020 13:55:10 +0000 Subject: [PATCH 14/22] NOTICK Update release-notes about database transaction limitations (#6052) * NOTICK Update release-notes about database transaction limitations --- docs/source/release-notes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 029f89a10b..d4fcf1ae94 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -79,6 +79,10 @@ Given the addition of new APIs, the platform version of Corda 4.4 has been bumpe For more information on platform version, please see :doc:`versioning`. For more details on upgrading a CorDapp to use platform version 5, please see :doc:`app-upgrade-notes`. +Known Issues +~~~~~~~~~~~~ + +Changes introduced in Corda 4.4 to increase ledger integrity have highlighted limitations regarding database transactions. To prevent flows from continuing to process after a database transaction has failed to commit or suffered from a pre-commit persistence exception, extra database flushes have been added. These extra flushes can cause exceptions to be thrown where they were not before (or cause different exception types to be raised compared to Corda 4.3 or previous versions). In general, CorDapp developers should not expect to be able to catch exceptions thrown during a database transaction and then continue with further DB operations as part of the same flow. A safer pattern involves allowing the flow to fail and be retried Issues Fixed ~~~~~~~~~~~~ From eba113621c68c5914fb34b3dc23b0e896711b2a7 Mon Sep 17 00:00:00 2001 From: Dimos Raptis Date: Tue, 17 Mar 2020 16:51:07 +0000 Subject: [PATCH 15/22] [CORDA-3628] - Avoid sending actions to the state machine if no messages are to be sent (#6074) --- .../kotlin/net/corda/node/services/statemachine/Action.kt | 6 +++++- .../statemachine/transitions/StartedFlowTransition.kt | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt index 14f8229fec..0c4bbe4b3a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt @@ -46,7 +46,11 @@ sealed class Action { data class SendMultiple( val sendInitial: List, val sendExisting: List - ): Action() + ): Action() { + init { + check(sendInitial.isNotEmpty() || sendExisting.isNotEmpty()) { "At least one of the lists with initial or existing session messages should contain items." } + } + } /** * Persist the specified [checkpoint]. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index 6f9956b692..4a10c335bd 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -289,7 +289,9 @@ class StartedFlowTransition( } } ?: emptyList() - actions.add(Action.SendMultiple(sendInitialActions, sendExistingActions)) + if (sendInitialActions.isNotEmpty() || sendExistingActions.isNotEmpty()) { + actions.add(Action.SendMultiple(sendInitialActions, sendExistingActions)) + } currentState = currentState.copy(checkpoint = checkpoint.copy(sessions = newSessions)) } From 349bd5a5116fc04029b0fd09a2bc7c42f2cd9af9 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Tue, 17 Mar 2020 17:02:08 +0000 Subject: [PATCH 16/22] CORDA-3662: Use an INNER JOIN for network map cache queries, (#6062) - rename add or update function for clarity - put removal of old nodes after retrieval of new ones to avoid gaps in the map - plus add a test --- .../network/PersistentNetworkMapCacheTest.kt | 30 ++--- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../node/services/api/ServiceHubInternal.kt | 7 +- .../services/network/NetworkMapUpdater.kt | 36 ++++-- .../network/PersistentNetworkMapCache.kt | 14 +-- .../corda/node/internal/NodeRestartTests.kt | 4 +- .../services/network/NetworkMapCacheTest.kt | 6 +- .../services/network/NetworkMapUpdaterTest.kt | 111 ++++++++++++++---- .../node/internal/InternalMockNetwork.kt | 4 +- .../testing/node/internal/NodeBasedTest.kt | 2 +- 10 files changed, 148 insertions(+), 68 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index f9675a94c1..9c701ccd3a 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -50,7 +50,7 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun addNode() { val alice = createNodeInfo(listOf(ALICE)) - charlieNetMapCache.addNode(alice) + charlieNetMapCache.addOrUpdateNode(alice) val fromDb = database.transaction { session.createQuery( "from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name}", @@ -62,7 +62,7 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `unknown legal name`() { - charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE))) assertThat(charlieNetMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).isEmpty() assertThat(charlieNetMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME)).isNull() assertThat(charlieNetMapCache.getPeerByLegalName(DUMMY_NOTARY_NAME)).isNull() @@ -71,13 +71,13 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `nodes in distributed service`() { - charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE))) val distributedIdentity = TestIdentity(DUMMY_NOTARY_NAME) val distServiceNodeInfos = (1..2).map { val nodeInfo = createNodeInfo(identities = listOf(TestIdentity.fresh("Org-$it"), distributedIdentity)) - charlieNetMapCache.addNode(nodeInfo) + charlieNetMapCache.addOrUpdateNode(nodeInfo) nodeInfo } @@ -90,7 +90,7 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `get nodes by owning key and by name`() { val alice = createNodeInfo(listOf(ALICE)) - charlieNetMapCache.addNode(alice) + charlieNetMapCache.addOrUpdateNode(alice) assertThat(charlieNetMapCache.getNodesByLegalIdentityKey(ALICE.publicKey)).containsOnly(alice) assertThat(charlieNetMapCache.getNodeByLegalName(ALICE.name)).isEqualTo(alice) } @@ -98,31 +98,31 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `get nodes by address`() { val alice = createNodeInfo(listOf(ALICE)) - charlieNetMapCache.addNode(alice) + charlieNetMapCache.addOrUpdateNode(alice) assertThat(charlieNetMapCache.getNodeByAddress(alice.addresses[0])).isEqualTo(alice) } @Test(timeout=300_000) fun `insert two node infos with the same host and port`() { val alice = createNodeInfo(listOf(ALICE)) - charlieNetMapCache.addNode(alice) + charlieNetMapCache.addOrUpdateNode(alice) val bob = createNodeInfo(listOf(BOB), address = alice.addresses[0]) - charlieNetMapCache.addNode(bob) + charlieNetMapCache.addOrUpdateNode(bob) val nodeInfos = charlieNetMapCache.allNodes.filter { alice.addresses[0] in it.addresses } assertThat(nodeInfos).hasSize(2) } @Test(timeout=300_000) fun `negative test - attempt to insert invalid node info`() { - charlieNetMapCache.addNode(createNodeInfo(listOf(LONG_PLC))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(LONG_PLC))) assertThat(charlieNetMapCache.allNodes).hasSize(0) } @Test(timeout=300_000) fun `negative test - attempt to update existing node with invalid node info`() { - charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE))) val aliceUpdate = TestIdentity(LONG_X500_NAME, ALICE.keyPair) - charlieNetMapCache.addNode(createNodeInfo(listOf(aliceUpdate))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(aliceUpdate))) assertThat(charlieNetMapCache.allNodes).hasSize(1) assertThat(charlieNetMapCache.getNodeByLegalName(ALICE_NAME)).isNotNull assertThat(charlieNetMapCache.getNodeByLegalName(LONG_X500_NAME)).isNull() @@ -130,7 +130,7 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `negative test - insert two valid node infos and one invalid one`() { - charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(ALICE)), + charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(ALICE)), createNodeInfo(listOf(BOB)), createNodeInfo(listOf(LONG_PLC)))) assertThat(charlieNetMapCache.allNodes).hasSize(2) @@ -139,7 +139,7 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `negative test - insert three valid node infos and two invalid ones`() { - charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(LONG_PLC)), + charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(LONG_PLC)), createNodeInfo(listOf(ALICE)), createNodeInfo(listOf(BOB)), createNodeInfo(listOf(CHARLIE)), @@ -150,9 +150,9 @@ class PersistentNetworkMapCacheTest { @Test(timeout=300_000) fun `negative test - insert one valid node info then attempt to add one invalid node info and update the existing valid nodeinfo`() { - charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE))) + charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE))) val aliceUpdate = TestIdentity(LONG_X500_NAME, ALICE.keyPair) - charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(aliceUpdate)), + charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(aliceUpdate)), createNodeInfo(listOf(LONGER_PLC)), createNodeInfo(listOf(BOB)))) assertThat(charlieNetMapCache.allNodes).hasSize(2) assertThat(charlieNetMapCache.getNodeByLegalName(ALICE_NAME)).isNotNull diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 3141b71bbc..045136c9b3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -633,7 +633,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } else { log.info("Node-info has changed so submitting update. Old node-info was $nodeInfoFromDb") val newNodeInfo = potentialNodeInfo.copy(serial = platformClock.millis()) - networkMapCache.addNode(newNodeInfo) + networkMapCache.addOrUpdateNode(newNodeInfo) log.info("New node-info: $newNodeInfo") newNodeInfo } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 984d0b216f..33ae457877 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -40,10 +40,11 @@ interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { * This is used for Artemis bridge lookup process. */ fun getNodesByOwningKeyIndex(identityKeyIndex: String): List - /** Adds a node to the local cache (generally only used for adding ourselves). */ - fun addNode(node: NodeInfo) + /** Adds (or updates) a node to the local cache (generally only used for adding ourselves). */ + fun addOrUpdateNode(node: NodeInfo) - fun addNodes(nodes: List) + /** Adds (or updates) nodes to the local cache. */ + fun addOrUpdateNodes(nodes: List) /** Removes a node from the local cache. */ fun removeNode(node: NodeInfo) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index d39f6fbc8f..7ff18232a2 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -4,7 +4,13 @@ import com.google.common.util.concurrent.MoreExecutors import net.corda.core.CordaRuntimeException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData -import net.corda.core.internal.* +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.copyTo +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.internal.readObject +import net.corda.core.internal.sign import net.corda.core.messaging.DataFeed import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.AutoAcceptable @@ -20,7 +26,12 @@ import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.utilities.NamedThreadFactory import net.corda.nodeapi.exceptions.OutdatedNetworkParameterHashException import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.nodeapi.internal.network.* +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME +import net.corda.nodeapi.internal.network.NetworkMap +import net.corda.nodeapi.internal.network.ParametersUpdate +import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert import rx.Subscription import rx.subjects.PublishSubject import java.lang.Integer.max @@ -118,7 +129,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, .subscribe { for (update in it) { when (update) { - is NodeInfoUpdate.Add -> networkMapCache.addNode(update.nodeInfo) + is NodeInfoUpdate.Add -> networkMapCache.addOrUpdateNode(update.nodeInfo) is NodeInfoUpdate.Remove -> { if (update.hash != ourNodeInfoHash) { val nodeInfo = networkMapCache.getNodeByHash(update.hash) @@ -177,11 +188,12 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, if (currentParametersHash != globalNetworkMap.networkParameterHash) { exitOnParametersMismatch(globalNetworkMap) } - val currentNodeHashes = networkMapCache.allNodeHashes - // Remove node info from network map. - (currentNodeHashes - allHashesFromNetworkMap - nodeInfoWatcher.processedNodeInfoHashes) - .mapNotNull { if (it != ourNodeInfoHash) networkMapCache.getNodeByHash(it) else null } - .forEach(networkMapCache::removeNode) + // Calculate any nodes that are now gone and remove _only_ them from the cache + // NOTE: We won't remove them until after the add/update cycle as only then will we definitely know which nodes are no longer + // in the network + val allNodeHashes = networkMapCache.allNodeHashes + val nodeHashesToBeDeleted = (allNodeHashes - allHashesFromNetworkMap - nodeInfoWatcher.processedNodeInfoHashes) + .filter { it != ourNodeInfoHash } //at the moment we use a blocking HTTP library - but under the covers, the OS will interleave threads waiting for IO //as HTTP GET is mostly IO bound, use more threads than CPU's //maximum threads to use = 24, as if we did not limit this on large machines it could result in 100's of concurrent requests @@ -189,7 +201,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, val executorToUseForDownloadingNodeInfos = Executors.newFixedThreadPool(threadsToUseForNetworkMapDownload, NamedThreadFactory("NetworkMapUpdaterNodeInfoDownloadThread")) //DB insert is single threaded - use a single threaded executor for it. val executorToUseForInsertionIntoDB = Executors.newSingleThreadExecutor(NamedThreadFactory("NetworkMapUpdateDBInsertThread")) - val hashesToFetch = (allHashesFromNetworkMap - currentNodeHashes) + val hashesToFetch = (allHashesFromNetworkMap - allNodeHashes) val networkMapDownloadStartTime = System.currentTimeMillis() if (hashesToFetch.isNotEmpty()) { val networkMapDownloadFutures = hashesToFetch.chunked(max(hashesToFetch.size / threadsToUseForNetworkMapDownload, 1)) @@ -207,7 +219,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } }, executorToUseForDownloadingNodeInfos).thenAcceptAsync(Consumer { retrievedNodeInfos -> // Add new node info to the network map cache, these could be new node info or modification of node info for existing nodes. - networkMapCache.addNodes(retrievedNodeInfos) + networkMapCache.addOrUpdateNodes(retrievedNodeInfos) }, executorToUseForInsertionIntoDB) }.toTypedArray() //wait for all the futures to complete @@ -218,6 +230,10 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, executorToUseForInsertionIntoDB.shutdown() }.getOrThrow() } + // NOTE: We remove nodes after any new/updates because updated nodes will have a new hash and, therefore, any + // nodes that we can actually pull out of the cache (with the old hashes) should be a truly removed node. + nodeHashesToBeDeleted.mapNotNull { networkMapCache.getNodeByHash(it) }.forEach(networkMapCache::removeNode) + // Mark the network map cache as ready on a successful poll of the HTTP network map, even on the odd chance that // it's empty networkMapCache.nodeReady.set(null) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index a201880c16..877a7f2aed 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -159,7 +159,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, } } - override fun addNodes(nodes: List) { + override fun addOrUpdateNodes(nodes: List) { synchronized(_changed) { val newNodes = mutableListOf() val updatedNodes = mutableListOf>() @@ -226,9 +226,9 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, } } - override fun addNode(node: NodeInfo) { + override fun addOrUpdateNode(node: NodeInfo) { logger.info("Adding node with info: $node") - addNodes(listOf(node)) + addOrUpdateNodes(listOf(node)) logger.debug { "Done adding node with info: $node" } } @@ -305,7 +305,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n INNER JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", NodeInfoSchemaV1.PersistentNodeInfo::class.java) query.setParameter("owningKeyHash", identityKeyIndex) return query.resultList @@ -323,7 +323,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private fun queryIdentityByLegalName(session: Session, name: CordaX500Name): PartyAndCertificate? { val query = session.createQuery( // We do the JOIN here to restrict results to those present in the network map - "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n INNER JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", NodeInfoSchemaV1.DBPartyAndCertificate::class.java) query.setParameter("name", name.toString()) val candidates = query.resultList.map { it.toLegalIdentityAndCert() } @@ -333,7 +333,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private fun queryByLegalName(session: Session, name: CordaX500Name): List { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n INNER JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", NodeInfoSchemaV1.PersistentNodeInfo::class.java) query.setParameter("name", name.toString()) val result = query.resultList @@ -342,7 +342,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.host = :host AND a.port = :port", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n INNER JOIN n.addresses a WHERE a.host = :host AND a.port = :port", NodeInfoSchemaV1.PersistentNodeInfo::class.java) query.setParameter("host", hostAndPort.host) query.setParameter("port", hostAndPort.port) diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeRestartTests.kt b/node/src/test/kotlin/net/corda/node/internal/NodeRestartTests.kt index 2da7661dd8..59d6a23d74 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeRestartTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeRestartTests.kt @@ -31,8 +31,8 @@ class NodeRestartTests { val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) val bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) bob.registerInitiatedFlow(Responder::class.java) - alice.services.networkMapCache.addNode(bob.info) - bob.services.networkMapCache.addNode(alice.info) + alice.services.networkMapCache.addOrUpdateNode(bob.info) + bob.services.networkMapCache.addOrUpdateNode(alice.info) val alice2 = mockNet.restartNode(alice) val result = alice2.services.startFlow(Initiator(bob.info.singleIdentity())).resultFuture.getOrThrow() assertThat(result).isEqualTo(123) diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index 0ba75cc799..b852a921db 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -37,7 +37,7 @@ class NetworkMapCacheTest { val bob = bobNode.info.singleIdentity() assertEquals(alice, bob) - aliceNode.services.networkMapCache.addNode(bobNode.info) + aliceNode.services.networkMapCache.addOrUpdateNode(bobNode.info) // The details of node B write over those for node A assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(alice.owningKey).singleOrNull(), bobNode.info) } @@ -86,7 +86,7 @@ class NetworkMapCacheTest { assertNull(bobCache.getPeerByLegalName(ALICE_NAME)) assertThat(bobCache.getNodesByLegalIdentityKey(aliceNode.info.singleIdentity().owningKey).isEmpty()) - bobCacheInternal.addNode(aliceNode.info) + bobCacheInternal.addOrUpdateNode(aliceNode.info) assertEquals(aliceNode.info.singleIdentity(), bobCache.getPeerByLegalName(ALICE_NAME)) assertEquals(aliceNode.info, bobCache.getNodesByLegalIdentityKey(aliceNode.info.singleIdentity().owningKey).single()) @@ -113,7 +113,7 @@ class NetworkMapCacheTest { val aliceNode = mockNet.createPartyNode(ALICE_NAME) val aliceCache = aliceNode.services.networkMapCache val alicePartyAndCert2 = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public) - aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(alicePartyAndCert2))) + aliceCache.addOrUpdateNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(alicePartyAndCert2))) // This is correct behaviour as we may have distributed service nodes. assertEquals(2, aliceCache.getNodesByLegalName(ALICE_NAME).size) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index dd47140877..d2689ce039 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -2,15 +2,26 @@ package net.corda.node.services.network import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs -import com.nhaarman.mockito_kotlin.* +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.never +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.NODE_INFO_DIRECTORY +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.delete +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.internal.readObject +import net.corda.core.internal.sign import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo @@ -18,6 +29,8 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.serialize import net.corda.core.utilities.millis import net.corda.coretesting.internal.DEV_ROOT_CA +import net.corda.coretesting.internal.TestNodeInfoBuilder +import net.corda.coretesting.internal.createNodeInfoAndSigned import net.corda.node.VersionInfo import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.NetworkParameterAcceptanceSettings @@ -29,16 +42,23 @@ import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.* -import net.corda.coretesting.internal.TestNodeInfoBuilder -import net.corda.coretesting.internal.createNodeInfoAndSigned +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.expect +import net.corda.testing.core.expectEvents +import net.corda.testing.core.sequence import net.corda.testing.node.internal.MockKeyManagementService import net.corda.testing.node.internal.network.NetworkMapServer import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.hamcrest.collection.IsIterableContainingInAnyOrder -import org.junit.* +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test import rx.schedulers.TestScheduler import java.io.IOException import java.net.URL @@ -118,7 +138,7 @@ class NetworkMapUpdaterTest { //Test adding new node. networkMapClient.publish(signedNodeInfo1) //Not subscribed yet. - verify(networkMapCache, times(0)).addNode(any()) + verify(networkMapCache, times(0)).addOrUpdateNode(any()) startUpdater() networkMapClient.publish(signedNodeInfo2) @@ -194,13 +214,56 @@ class NetworkMapUpdaterTest { assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) } + @Test(timeout=300_000) + fun `process remove, add, and update node from network map`() { + setUpdater() + val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1") + val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") + + val builder = TestNodeInfoBuilder() + builder.addLegalIdentity(CordaX500Name("Test", "London", "GB")) + val (nodeInfo2, signedNodeInfo2) = builder.buildWithSigned(1) + val (nodeInfo2_2, signedNodeInfo2_2) = builder.buildWithSigned(2) + + //Add all nodes. + networkMapClient.publish(signedNodeInfo1) + networkMapClient.publish(signedNodeInfo2) + + startUpdater() + advanceTime() + //TODO: Remove sleep in unit test. + Thread.sleep(2L * cacheExpiryMs) + + Assert.assertThat(networkMapCache.allNodeHashes, IsIterableContainingInAnyOrder.containsInAnyOrder( + signedNodeInfo1.raw.hash, + signedNodeInfo2.raw.hash + )) + + // remove one node, add another and update a third. + server.removeNodeInfo(nodeInfo1) + networkMapClient.publish(signedNodeInfo3) + networkMapClient.publish(signedNodeInfo2_2) + + advanceTime() + //TODO: Remove sleep in unit test. + Thread.sleep(2L * cacheExpiryMs) + verify(networkMapCache, times(1)).removeNode(nodeInfo1) + verify(networkMapCache, times(0)).removeNode(nodeInfo2) + verify(networkMapCache, times(1)).addOrUpdateNodes(listOf(nodeInfo2_2)) + verify(networkMapCache, times(1)).addOrUpdateNodes(listOf(nodeInfo3)) + assertThat(networkMapCache.allNodeHashes).hasSameElementsAs(listOf( + signedNodeInfo2_2.raw.hash, + signedNodeInfo3.raw.hash + )) + } + @Test(timeout=300_000) fun `receive node infos from directory, without a network map`() { setUpdater(netMapClient = null) val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") //Not subscribed yet. - verify(networkMapCache, times(0)).addNode(any()) + verify(networkMapCache, times(0)).addOrUpdateNode(any()) startUpdater() @@ -208,8 +271,8 @@ class NetworkMapUpdaterTest { assertThat(nodeReadyFuture).isNotDone() advanceTime() - verify(networkMapCache, times(1)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) + verify(networkMapCache, times(1)).addOrUpdateNode(any()) + verify(networkMapCache, times(1)).addOrUpdateNode(fileNodeInfoAndSigned.nodeInfo) assertThat(nodeReadyFuture).isDone() assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) @@ -330,9 +393,9 @@ class NetworkMapUpdaterTest { NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned1) NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned2) advanceTime() - verify(networkMapCache, times(2)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned1.nodeInfo) - verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned2.nodeInfo) + verify(networkMapCache, times(2)).addOrUpdateNode(any()) + verify(networkMapCache, times(1)).addOrUpdateNode(fileNodeInfoAndSigned1.nodeInfo) + verify(networkMapCache, times(1)).addOrUpdateNode(fileNodeInfoAndSigned2.nodeInfo) assertThat(networkMapCache.allNodeHashes).containsExactlyInAnyOrder(fileNodeInfoAndSigned1.signed.raw.hash, fileNodeInfoAndSigned2.signed.raw.hash) //Remove one of the nodes val fileName1 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${fileNodeInfoAndSigned1.nodeInfo.legalIdentities[0].name.serialize().hash}" @@ -360,7 +423,7 @@ class NetworkMapUpdaterTest { networkMapClient.publish(serverSignedNodeInfo) startUpdater() advanceTime() - verify(networkMapCache, times(1)).addNode(localNodeInfo) + verify(networkMapCache, times(1)).addOrUpdateNode(localNodeInfo) Thread.sleep(2L * cacheExpiryMs) //Node from file has higher serial than the one from NetworkMapServer assertThat(networkMapCache.allNodeHashes).containsOnly(localSignedNodeInfo.signed.raw.hash) @@ -382,7 +445,7 @@ class NetworkMapUpdaterTest { val (myInfo, signedMyInfo) = createNodeInfoAndSigned("My node info") val (_, signedOtherInfo) = createNodeInfoAndSigned("Other info") setUpdater() - networkMapCache.addNode(myInfo) //Simulate behaviour on node startup when our node info is added to cache + networkMapCache.addOrUpdateNode(myInfo) //Simulate behaviour on node startup when our node info is added to cache networkMapClient.publish(signedOtherInfo) startUpdater(ourNodeInfo = signedMyInfo) Thread.sleep(2L * cacheExpiryMs) @@ -405,19 +468,22 @@ class NetworkMapUpdaterTest { //Test adding new node. networkMapClient.publish(signedNodeInfo1) //Not subscribed yet. - verify(networkMapCache, times(0)).addNode(any()) + verify(networkMapCache, times(0)).addOrUpdateNode(any()) startUpdater() //TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) assert(networkMapCache.allNodeHashes.size == 1) + assert(networkMapCache.allNodeHashes.first() == signedNodeInfo1.raw.hash) + verify(networkMapCache, times(1)).addOrUpdateNodes(listOf(signedNodeInfo1.verified())) networkMapClient.publish(signedNodeInfo2) Thread.sleep(2L * cacheExpiryMs) advanceTime() - verify(networkMapCache, times(1)).removeNode(signedNodeInfo1.verified()) + verify(networkMapCache, times(1)).addOrUpdateNodes(listOf(signedNodeInfo1.verified())) assert(networkMapCache.allNodeHashes.size == 1) + assert(networkMapCache.allNodeHashes.first() == signedNodeInfo2.raw.hash) } @Test(timeout=300_000) @@ -458,15 +524,11 @@ class NetworkMapUpdaterTest { return mock { on { nodeReady }.thenReturn(nodeReadyFuture) val data = ConcurrentHashMap() - on { addNode(any()) }.then { + on { addOrUpdateNode(any()) }.then { val nodeInfo = it.arguments[0] as NodeInfo - val party = nodeInfo.legalIdentities[0] - data.compute(party) { _, current -> - if (current == null || current.serial < nodeInfo.serial) nodeInfo else current - } + addNodeToMockCache(nodeInfo, data) } - - on { addNodes(any>()) }.then { + on { addOrUpdateNodes(any()) }.then { @Suppress("UNCHECKED_CAST") val nodeInfos = it.arguments[0] as List nodeInfos.forEach { nodeInfo -> @@ -476,6 +538,7 @@ class NetworkMapUpdaterTest { on { removeNode(any()) }.then { data.remove((it.arguments[0] as NodeInfo).legalIdentities[0]) } on { getNodeByLegalIdentity(any()) }.then { data[it.arguments[0]] } + on { allNodes }.then { data.values.toList() } on { allNodeHashes }.then { data.values.map { it.serialize().hash } } on { getNodeByHash(any()) }.then { mock -> data.values.singleOrNull { it.serialize().hash == mock.arguments[0] } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 464e08966f..855db08b1f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -352,8 +352,8 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), mockNet.nodes .mapNotNull { it.started } .forEach { existingNode -> - newNode.services.networkMapCache.addNode(existingNode.info) - existingNode.services.networkMapCache.addNode(newNode.info) + newNode.services.networkMapCache.addOrUpdateNode(existingNode.info) + existingNode.services.networkMapCache.addOrUpdateNode(newNode.info) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index b53a83c97f..66d96ab222 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -147,7 +147,7 @@ constructor(private val cordappPackages: List = emptyList(), private val val runningNodesInfo = runningNodes.map { it.info } for (node in runningNodes) for (nodeInfo in runningNodesInfo) { - node.services.networkMapCache.addNode(nodeInfo) + node.services.networkMapCache.addOrUpdateNode(nodeInfo) } } } From f9ccb88feaedcc749196a36c8277bee5e3e1f835 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Wed, 18 Mar 2020 09:37:56 +0000 Subject: [PATCH 17/22] ENT-4494 Harmonize Kryo serialalization (#6069) * Harmonize Kryo serialalization (Custom serializer for iterators/collections) * Fix package name * Revert checkpoint compression change. * Clean imports --- .../kryo/CustomIteratorSerializers.kt | 121 ++++++++++++++++ .../kryo/DefaultKryoCustomizer.kt | 24 +++- .../internal/serialization/kryo/Kryo.kt | 16 ++- .../kryo/KryoCheckpointSerializer.kt | 13 +- .../serialization/kryo/KryoCheckpointTest.kt | 132 ++++++++++++++++++ 5 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/CustomIteratorSerializers.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/CustomIteratorSerializers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/CustomIteratorSerializers.kt new file mode 100644 index 0000000000..ff50a1c137 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/CustomIteratorSerializers.kt @@ -0,0 +1,121 @@ +package net.corda.nodeapi.internal.serialization.kryo + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.util.LinkedList + +/** + * The [LinkedHashMap] and [LinkedHashSet] have a problem with the default Quasar/Kryo serialisation + * in that serialising an iterator (and subsequent [LinkedHashMap.Entry]) over a sufficiently large + * data set can lead to a stack overflow (because the object map is traversed recursively). + * + * We've added our own custom serializer in order to ensure that the iterator is correctly deserialized. + */ +internal object LinkedHashMapIteratorSerializer : Serializer>() { + private val DUMMY_MAP = linkedMapOf(1L to 1) + private val outerMapField: Field = getIterator()::class.java.superclass.getDeclaredField("this$0").apply { isAccessible = true } + private val currentField: Field = getIterator()::class.java.superclass.getDeclaredField("current").apply { isAccessible = true } + + private val KEY_ITERATOR_CLASS: Class> = DUMMY_MAP.keys.iterator().javaClass + private val VALUE_ITERATOR_CLASS: Class> = DUMMY_MAP.values.iterator().javaClass + private val MAP_ITERATOR_CLASS: Class>> = DUMMY_MAP.iterator().javaClass + + fun getIterator(): Any = DUMMY_MAP.iterator() + + override fun write(kryo: Kryo, output: Output, obj: Iterator<*>) { + val current: Map.Entry<*, *>? = currentField.get(obj) as Map.Entry<*, *>? + kryo.writeClassAndObject(output, outerMapField.get(obj)) + kryo.writeClassAndObject(output, current) + } + + override fun read(kryo: Kryo, input: Input, type: Class>): Iterator<*> { + val outerMap = kryo.readClassAndObject(input) as Map<*, *> + return when (type) { + KEY_ITERATOR_CLASS -> { + val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>)?.key + outerMap.keys.iterator().returnToIteratorLocation(current) + } + VALUE_ITERATOR_CLASS -> { + val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>)?.value + outerMap.values.iterator().returnToIteratorLocation(current) + } + MAP_ITERATOR_CLASS -> { + val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>) + outerMap.iterator().returnToIteratorLocation(current) + } + else -> throw IllegalStateException("Invalid type") + } + } + + private fun Iterator<*>.returnToIteratorLocation(current: Any?) : Iterator<*> { + while (this.hasNext()) { + val key = this.next() + @Suppress("SuspiciousEqualsCombination") + if (current == null || key === current || key == current) { + break + } + } + return this + } +} + +/** + * The [LinkedHashMap] and [LinkedHashSet] have a problem with the default Quasar/Kryo serialisation + * in that serialising an iterator (and subsequent [LinkedHashMap.Entry]) over a sufficiently large + * data set can lead to a stack overflow (because the object map is traversed recursively). + * + * We've added our own custom serializer in order to ensure that only the key/value are recorded. + * The rest of the list isn't required at this scope. + */ +object LinkedHashMapEntrySerializer : Serializer>() { + // Create a dummy map so that we can get the LinkedHashMap$Entry from it + // The element type of the map doesn't matter. The entry is all we want + private val DUMMY_MAP = linkedMapOf(1L to 1) + fun getEntry(): Any = DUMMY_MAP.entries.first() + private val constr: Constructor<*> = getEntry()::class.java.declaredConstructors.single().apply { isAccessible = true } + + /** + * Kryo would end up serialising "this" entry, then serialise "this.after" recursively, leading to a very large stack. + * we'll skip that and just write out the key/value + */ + override fun write(kryo: Kryo, output: Output, obj: Map.Entry<*, *>) { + val e: Map.Entry<*, *> = obj + kryo.writeClassAndObject(output, e.key) + kryo.writeClassAndObject(output, e.value) + } + + override fun read(kryo: Kryo, input: Input, type: Class>): Map.Entry<*, *> { + val key = kryo.readClassAndObject(input) + val value = kryo.readClassAndObject(input) + return constr.newInstance(0, key, value, null) as Map.Entry<*, *> + } +} + +/** + * Also, add a [ListIterator] serializer to avoid more linked list issues. +*/ +object LinkedListItrSerializer : Serializer>() { + // Create a dummy list so that we can get the ListItr from it + // The element type of the list doesn't matter. The iterator is all we want + private val DUMMY_LIST = LinkedList(listOf(1)) + fun getListItr(): Any = DUMMY_LIST.listIterator() + + private val outerListField: Field = getListItr()::class.java.getDeclaredField("this$0").apply { isAccessible = true } + + override fun write(kryo: Kryo, output: Output, obj: ListIterator) { + kryo.writeClassAndObject(output, outerListField.get(obj)) + output.writeInt(obj.nextIndex()) + } + + override fun read(kryo: Kryo, input: Input, type: Class>): ListIterator { + val list = kryo.readClassAndObject(input) as LinkedList<*> + val index = input.readInt() + return list.listIterator(index) + } +} + + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt index 615f9a74f5..cf1728c34f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt @@ -10,7 +10,11 @@ import com.esotericsoftware.kryo.serializers.FieldSerializer import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer -import de.javakaffee.kryoserializers.guava.* +import de.javakaffee.kryoserializers.guava.ImmutableListSerializer +import de.javakaffee.kryoserializers.guava.ImmutableMapSerializer +import de.javakaffee.kryoserializers.guava.ImmutableMultimapSerializer +import de.javakaffee.kryoserializers.guava.ImmutableSetSerializer +import de.javakaffee.kryoserializers.guava.ImmutableSortedSetSerializer import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.PrivacySalt @@ -24,7 +28,11 @@ import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes -import net.corda.core.transactions.* +import net.corda.core.transactions.ContractUpgradeFilteredTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.toNonEmptySet import net.corda.serialization.internal.DefaultWhitelist @@ -51,8 +59,9 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.X509Certificate -import java.util.* -import kotlin.collections.ArrayList +import java.util.Arrays +import java.util.BitSet +import java.util.ServiceLoader object DefaultKryoCustomizer { private val serializationWhitelists: List by lazy { @@ -70,7 +79,8 @@ object DefaultKryoCustomizer { instantiatorStrategy = CustomInstantiatorStrategy() // Required for HashCheckingStream (de)serialization. - // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). + // Note that return type should be specifically set to InputStream, otherwise it may not work, + // i.e. val aStream : InputStream = HashCheckingStream(...). addDefaultSerializer(InputStream::class.java, InputStreamSerializer) addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) addDefaultSerializer(Logger::class.java, LoggerSerializer) @@ -79,8 +89,10 @@ object DefaultKryoCustomizer { // WARNING: reordering the registrations here will cause a change in the serialized form, since classes // with custom serializers get written as registration ids. This will break backwards-compatibility. // Please add any new registrations to the end. - // TODO: re-organise registrations into logical groups before v1.0 + addDefaultSerializer(LinkedHashMapIteratorSerializer.getIterator()::class.java.superclass, LinkedHashMapIteratorSerializer) + register(LinkedHashMapEntrySerializer.getEntry()::class.java, LinkedHashMapEntrySerializer) + register(LinkedListItrSerializer.getListItr()::class.java, LinkedListItrSerializer) register(Arrays.asList("").javaClass, ArraysAsListSerializer()) register(LazyMappedList::class.java, LazyMappedListSerializer) register(SignedTransaction::class.java, SignedTransactionSerializer) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index e5de8a1341..929fa63a8e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -18,6 +18,8 @@ import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes +import net.corda.serialization.internal.checkUseCase +import net.corda.core.utilities.SgxSupport import net.corda.serialization.internal.serializationContextKey import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -67,13 +69,17 @@ object SerializedBytesSerializer : Serializer>() { * set via the constructor and the class is immutable. */ class ImmutableClassSerializer(val klass: KClass) : Serializer() { - val props = klass.memberProperties.sortedBy { it.name } - val propsByName = props.associateBy { it.name } - val constructor = klass.primaryConstructor!! + val props by lazy { klass.memberProperties.sortedBy { it.name } } + val propsByName by lazy { props.associateBy { it.name } } + val constructor by lazy { klass.primaryConstructor!! } init { - props.forEach { - require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" } + // Verify that this class is immutable (all properties are final). + // We disable this check inside SGX as the reflection blows up. + if (!SgxSupport.isInsideEnclave) { + props.forEach { + require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" } + } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointSerializer.kt index 7dddc2a65f..6a73119ce6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointSerializer.kt @@ -10,11 +10,20 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool import com.esotericsoftware.kryo.serializers.ClosureSerializer import net.corda.core.internal.uncheckedCast -import net.corda.core.serialization.* +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.CheckpointSerializer import net.corda.core.utilities.ByteSequence -import net.corda.serialization.internal.* +import net.corda.serialization.internal.AlwaysAcceptEncodingWhitelist +import net.corda.serialization.internal.ByteBufferInputStream +import net.corda.serialization.internal.CheckpointSerializationContextImpl +import net.corda.serialization.internal.CordaSerializationEncoding +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.QuasarWhitelist +import net.corda.serialization.internal.SectionId +import net.corda.serialization.internal.encodingNotPermittedFormat import java.util.concurrent.ConcurrentHashMap val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0)) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointTest.kt new file mode 100644 index 0000000000..70ab07f54a --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoCheckpointTest.kt @@ -0,0 +1,132 @@ +package net.corda.nodeapi.internal.serialization.kryo + +import org.junit.Test +import org.junit.jupiter.api.assertDoesNotThrow +import java.util.LinkedList +import kotlin.test.assertEquals + +class KryoCheckpointTest { + + private val testSize = 1000L + + /** + * This test just ensures that the checkpoints still work in light of [LinkedHashMapEntrySerializer]. + */ + @Test(timeout=300_000) + fun `linked hash map can checkpoint without error`() { + var lastKey = "" + val dummyMap = linkedMapOf() + for (i in 0..testSize) { + dummyMap[i.toString()] = i + } + var it = dummyMap.iterator() + while (it.hasNext()) { + lastKey = it.next().key + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + assertEquals(testSize.toString(), lastKey) + } + + @Test(timeout=300_000) + fun `empty linked hash map can checkpoint without error`() { + val dummyMap = linkedMapOf() + val it = dummyMap.iterator() + val itKeys = dummyMap.keys.iterator() + val itValues = dummyMap.values.iterator() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + val bytesKeys = KryoCheckpointSerializer.serialize(itKeys, KRYO_CHECKPOINT_CONTEXT) + val bytesValues = KryoCheckpointSerializer.serialize(itValues, KRYO_CHECKPOINT_CONTEXT) + assertDoesNotThrow { + KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + KryoCheckpointSerializer.deserialize(bytesKeys, itKeys.javaClass, KRYO_CHECKPOINT_CONTEXT) + KryoCheckpointSerializer.deserialize(bytesValues, itValues.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + } + + @Test(timeout=300_000) + fun `linked hash map with null values can checkpoint without error`() { + val dummyMap = linkedMapOf().apply { + put(null, null) + } + val it = dummyMap.iterator() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + + val itKeys = dummyMap.keys.iterator() + val bytesKeys = KryoCheckpointSerializer.serialize(itKeys, KRYO_CHECKPOINT_CONTEXT) + + val itValues = dummyMap.values.iterator() + val bytesValues = KryoCheckpointSerializer.serialize(itValues, KRYO_CHECKPOINT_CONTEXT) + + assertDoesNotThrow { + KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + KryoCheckpointSerializer.deserialize(bytesKeys, itKeys.javaClass, KRYO_CHECKPOINT_CONTEXT) + KryoCheckpointSerializer.deserialize(bytesValues, itValues.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + } + + @Test(timeout=300_000) + fun `linked hash map keys can checkpoint without error`() { + var lastKey = "" + val dummyMap = linkedMapOf() + for (i in 0..testSize) { + dummyMap[i.toString()] = i + } + var it = dummyMap.keys.iterator() + while (it.hasNext()) { + lastKey = it.next() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + assertEquals(testSize.toString(), lastKey) + } + + @Test(timeout=300_000) + fun `linked hash map values can checkpoint without error`() { + var lastValue = 0L + val dummyMap = linkedMapOf() + for (i in 0..testSize) { + dummyMap[i.toString()] = i + } + var it = dummyMap.values.iterator() + while (it.hasNext()) { + lastValue = it.next() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + assertEquals(testSize, lastValue) + } + + /** + * This test just ensures that the checkpoints still work in light of [LinkedHashMapEntrySerializer]. + */ + @Test(timeout=300_000) + fun `linked hash set can checkpoint without error`() { + var result: Any = 0L + val dummySet = linkedSetOf().apply { addAll(0..testSize) } + var it = dummySet.iterator() + while (it.hasNext()) { + result = it.next() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + assertEquals(testSize, result) + } + + /** + * This test just ensures that the checkpoints still work in light of [LinkedListItrSerializer]. + */ + @Test(timeout=300_000) + fun `linked list can checkpoint without error`() { + var result: Any = 0L + val dummyList = LinkedList().apply { addAll(0..testSize) } + + var it = dummyList.iterator() + while (it.hasNext()) { + result = it.next() + val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT) + it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT) + } + assertEquals(testSize, result) + } +} From fb64e47326c5a006d68ab8a3a78214b54239b9c6 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Wed, 18 Mar 2020 13:56:51 +0000 Subject: [PATCH 18/22] NOTICK Increase timeouts in external operation tests (#6076) * NOTICK Increase timeouts in external operation tests It seems these tests are timing out when they shouldn't be when running on the CI servers. To resolve this, the timeouts of `getOrThrow` calls have been increased to a minute. Reliance on timeouts has also been reduced through using locks. This should speed up tests that don't need to wait for the timeout anymore to progress. * NOTICK Remove hospital checking code * NOTICK Change locking code --- .../FlowExternalOperationInJavaTest.java | 25 ++-- .../AbstractFlowExternalOperationTest.kt | 55 +++---- .../flows/FlowExternalAsyncOperationTest.kt | 115 ++++++--------- .../FlowExternalOperationStartFlowTest.kt | 23 ++- .../flows/FlowExternalOperationTest.kt | 139 +++++++----------- 5 files changed, 149 insertions(+), 208 deletions(-) diff --git a/core-tests/src/test/java/net/corda/coretests/flows/FlowExternalOperationInJavaTest.java b/core-tests/src/test/java/net/corda/coretests/flows/FlowExternalOperationInJavaTest.java index 74613e5d96..ec41941360 100644 --- a/core-tests/src/test/java/net/corda/coretests/flows/FlowExternalOperationInJavaTest.java +++ b/core-tests/src/test/java/net/corda/coretests/flows/FlowExternalOperationInJavaTest.java @@ -23,7 +23,6 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import static net.corda.testing.driver.Driver.driver; -import static org.junit.Assert.assertEquals; public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperationTest { @@ -32,16 +31,16 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati driver(new DriverParameters().withStartNodesInProcess(true), driver -> { NodeHandle alice = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); NodeHandle bob = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); return KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic( FlowWithExternalOperationInJava.class, TestUtils.singleIdentity(bob.getNodeInfo()) - ).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS)); + ).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES)); }); } @@ -50,16 +49,16 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati driver(new DriverParameters().withStartNodesInProcess(true), driver -> { NodeHandle alice = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); NodeHandle bob = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); return KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic( FlowWithExternalAsyncOperationInJava.class, TestUtils.singleIdentity(bob.getNodeInfo()) - ).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS)); + ).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES)); }); } @@ -68,22 +67,18 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati driver(new DriverParameters().withStartNodesInProcess(true), driver -> { NodeHandle alice = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); NodeHandle bob = KotlinUtilsKt.getOrThrow( driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)), - Duration.of(20, ChronoUnit.SECONDS) + Duration.of(1, ChronoUnit.MINUTES) ); KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic( FlowWithExternalOperationThatGetsRetriedInJava.class, TestUtils.singleIdentity(bob.getNodeInfo()) - ).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS)); + ).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES)); - HospitalCounts counts = KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic( - GetHospitalCountersFlow.class - ).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS)); - assertEquals(1, counts.getDischarge()); - assertEquals(0, counts.getObservation()); + assertHospitalCounters(1, 0); return null; }); diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/AbstractFlowExternalOperationTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/AbstractFlowExternalOperationTest.kt index 52d963678d..735db12c64 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/AbstractFlowExternalOperationTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/AbstractFlowExternalOperationTest.kt @@ -12,26 +12,54 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByService import net.corda.core.identity.Party import net.corda.core.internal.concurrent.doOnComplete +import net.corda.core.messaging.FlowHandle import net.corda.core.node.AppServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema -import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.node.services.statemachine.StaffedFlowHospital +import org.junit.Before import java.sql.SQLTransientConnectionException import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors +import java.util.concurrent.Semaphore import java.util.function.Supplier import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Table +import kotlin.test.assertEquals abstract class AbstractFlowExternalOperationTest { + var dischargeCounter = 0 + var observationCounter = 0 + + @Before + fun before() { + StaffedFlowHospital.onFlowDischarged.clear() + StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargeCounter } + StaffedFlowHospital.onFlowKeptForOvernightObservation.clear() + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> ++observationCounter } + dischargeCounter = 0 + observationCounter = 0 + } + + fun blockUntilFlowKeptInForObservation(flow: () -> FlowHandle<*>) { + val lock = Semaphore(0) + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> lock.release() } + flow() + lock.acquire() + } + + fun assertHospitalCounters(discharge: Int, observation: Int) { + assertEquals(discharge, dischargeCounter) + assertEquals(observation, observationCounter) + } + @StartableByRPC @InitiatingFlow @StartableByService @@ -182,31 +210,6 @@ abstract class AbstractFlowExternalOperationTest { object CustomMappedSchema : MappedSchema(CustomSchema::class.java, 1, listOf(CustomTableEntity::class.java)) - // Internal use for testing only!! - @StartableByRPC - class GetHospitalCountersFlow : FlowLogic() { - override fun call(): HospitalCounts = - HospitalCounts( - serviceHub.cordaService(HospitalCounter::class.java).dischargeCounter, - serviceHub.cordaService(HospitalCounter::class.java).observationCounter - ) - } - - @CordaSerializable - data class HospitalCounts(val discharge: Int, val observation: Int) - - @Suppress("UNUSED_PARAMETER") - @CordaService - class HospitalCounter(services: AppServiceHub) : SingletonSerializeAsToken() { - var observationCounter: Int = 0 - var dischargeCounter: Int = 0 - - init { - StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargeCounter } - StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> ++observationCounter } - } - } - class MyCordaException(message: String) : CordaException(message) class DirectlyAccessedServiceHubException : CordaException("Null pointer from accessing flow's serviceHub") diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalAsyncOperationTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalAsyncOperationTest.kt index a9d7438347..cec2568214 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalAsyncOperationTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalAsyncOperationTest.kt @@ -6,12 +6,7 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.DirectlyAccessedServiceHubException -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.ExternalAsyncOperation -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FlowWithExternalProcess -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FutureService -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.MyCordaException +import net.corda.core.utilities.minutes import net.corda.node.services.statemachine.StateTransitionException import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -21,28 +16,24 @@ import net.corda.testing.driver.driver import org.junit.Test import java.sql.SQLTransientConnectionException import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeoutException -import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() { - @Test(timeout=300_000) - fun `external async operation`() { + @Test(timeout = 300_000) + fun `external async operation`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() alice.rpc.startFlow(::FlowWithExternalAsyncOperation, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(20.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + .returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external async operation that checks deduplicationId is not rerun when flow is retried`() { + @Test(timeout = 300_000) + fun `external async operation that checks deduplicationId is not rerun when flow is retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -50,16 +41,14 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() { alice.rpc.startFlow( ::FlowWithExternalAsyncOperationWithDeduplication, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(1, discharged) - assertEquals(0, observation) + assertHospitalCounters(1, 0) } } - @Test(timeout=300_000) - fun `external async operation propagates exception to calling flow`() { + @Test(timeout = 300_000) + fun `external async operation propagates exception to calling flow`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -68,100 +57,88 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() { ::FlowWithExternalAsyncOperationPropagatesException, bob.nodeInfo.singleIdentity(), MyCordaException::class.java - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external async operation exception can be caught in flow`() { + @Test(timeout = 300_000) + fun `external async operation exception can be caught in flow`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() val result = alice.rpc.startFlow( ::FlowWithExternalAsyncOperationThatThrowsExceptionAndCaughtInFlow, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) assertTrue(result as Boolean) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external async operation with exception that hospital keeps for observation does not fail`() { + @Test(timeout = 300_000) + fun `external async operation with exception that hospital keeps for observation does not fail`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalAsyncOperationPropagatesException, bob.nodeInfo.singleIdentity(), HospitalizeFlowException::class.java - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(1, observation) + assertHospitalCounters(0, 1) } } - @Test(timeout=300_000) - fun `external async operation with exception that hospital discharges is retried and runs the future again`() { + @Test(timeout = 300_000) + fun `external async operation with exception that hospital discharges is retried and runs the future again`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalAsyncOperationPropagatesException, bob.nodeInfo.singleIdentity(), SQLTransientConnectionException::class.java - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(3, discharged) - assertEquals(1, observation) + assertHospitalCounters(3, 1) } } - @Test(timeout=300_000) - fun `external async operation that throws exception rather than completing future exceptionally fails with internal exception`() { + @Test(timeout = 300_000) + fun `external async operation that throws exception rather than completing future exceptionally fails with internal exception`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() assertFailsWith { alice.rpc.startFlow(::FlowWithExternalAsyncOperationUnhandledException, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external async operation that passes serviceHub into process can be retried`() { + @Test(timeout = 300_000) + fun `external async operation that passes serviceHub into process can be retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalAsyncOperationThatPassesInServiceHubCanRetry, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(3, discharged) - assertEquals(1, observation) + assertHospitalCounters(3, 1) } } - @Test(timeout=300_000) - fun `external async operation that accesses serviceHub from flow directly will fail when retried`() { + @Test(timeout = 300_000) + fun `external async operation that accesses serviceHub from flow directly will fail when retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -169,23 +146,19 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() { alice.rpc.startFlow( ::FlowWithExternalAsyncOperationThatDirectlyAccessesServiceHubFailsRetry, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(1, discharged) - assertEquals(0, observation) + assertHospitalCounters(1, 0) } } - @Test(timeout=300_000) - fun `starting multiple futures and joining on their results`() { + @Test(timeout = 300_000) + fun `starting multiple futures and joining on their results`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - alice.rpc.startFlow(::FlowThatStartsMultipleFuturesAndJoins, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + alice.rpc.startFlow(::FlowThatStartsMultipleFuturesAndJoins, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationStartFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationStartFlowTest.kt index a1385d5563..bb80313c86 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationStartFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationStartFlowTest.kt @@ -5,40 +5,35 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds +import net.corda.core.utilities.minutes import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.junit.Test -import kotlin.test.assertEquals class FlowExternalOperationStartFlowTest : AbstractFlowExternalOperationTest() { - @Test(timeout=300_000) - fun `starting a flow inside of a flow that starts a future will succeed`() { + @Test(timeout = 300_000) + fun `starting a flow inside of a flow that starts a future will succeed`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() alice.rpc.startFlow(::FlowThatStartsAnotherFlowInAnExternalOperation, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(40.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + .returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `multiple flows can be started and their futures joined from inside a flow`() { + @Test(timeout = 300_000) + fun `multiple flows can be started and their futures joined from inside a flow`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() alice.rpc.startFlow(::ForkJoinFlows, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(40.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + .returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationTest.kt index 3d6fdea190..1b78e95732 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FlowExternalOperationTest.kt @@ -10,13 +10,8 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.CustomTableEntity -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.DirectlyAccessedServiceHubException -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.ExternalOperation -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FlowWithExternalProcess -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FutureService -import net.corda.coretests.flows.AbstractFlowExternalOperationTest.MyCordaException +import net.corda.core.utilities.minutes +import net.corda.node.services.statemachine.StaffedFlowHospital import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME @@ -26,30 +21,26 @@ import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.internal.cordappsForPackages import org.junit.Test -import java.lang.IllegalStateException import java.sql.SQLTransientConnectionException -import java.util.concurrent.TimeoutException -import kotlin.test.assertEquals +import java.util.concurrent.Semaphore import kotlin.test.assertFailsWith import kotlin.test.assertTrue class FlowExternalOperationTest : AbstractFlowExternalOperationTest() { - @Test(timeout=300_000) - fun `external operation`() { + @Test(timeout = 300_000) + fun `external operation`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() alice.rpc.startFlow(::FlowWithExternalOperation, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(20.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + .returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external operation that checks deduplicationId is not rerun when flow is retried`() { + @Test(timeout = 300_000) + fun `external operation that checks deduplicationId is not rerun when flow is retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -57,16 +48,14 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() { alice.rpc.startFlow( ::FlowWithExternalOperationWithDeduplication, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(1, discharged) - assertEquals(0, observation) + assertHospitalCounters(1, 0) } } - @Test(timeout=300_000) - fun `external operation propagates exception to calling flow`() { + @Test(timeout = 300_000) + fun `external operation propagates exception to calling flow`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -75,82 +64,72 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() { ::FlowWithExternalOperationPropagatesException, bob.nodeInfo.singleIdentity(), MyCordaException::class.java - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external operation exception can be caught in flow`() { + @Test(timeout = 300_000) + fun `external operation exception can be caught in flow`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() alice.rpc.startFlow(::FlowWithExternalOperationThatThrowsExceptionAndCaughtInFlow, bob.nodeInfo.singleIdentity()) - .returnValue.getOrThrow(20.seconds) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(0, observation) + .returnValue.getOrThrow(1.minutes) + assertHospitalCounters(0, 0) } } - @Test(timeout=300_000) - fun `external operation with exception that hospital keeps for observation does not fail`() { + @Test(timeout = 300_000) + fun `external operation with exception that hospital keeps for observation does not fail`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalOperationPropagatesException, bob.nodeInfo.singleIdentity(), HospitalizeFlowException::class.java - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(0, discharged) - assertEquals(1, observation) + assertHospitalCounters(0, 1) } } - @Test(timeout=300_000) - fun `external operation with exception that hospital discharges is retried and runs the external operation again`() { + @Test(timeout = 300_000) + fun `external operation with exception that hospital discharges is retried and runs the external operation again`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalOperationPropagatesException, bob.nodeInfo.singleIdentity(), SQLTransientConnectionException::class.java - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(3, discharged) - assertEquals(1, observation) + assertHospitalCounters(3, 1) } } - @Test(timeout=300_000) - fun `external async operation that passes serviceHub into process can be retried`() { + @Test(timeout = 300_000) + fun `external async operation that passes serviceHub into process can be retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() - assertFailsWith { + blockUntilFlowKeptInForObservation { alice.rpc.startFlow( ::FlowWithExternalOperationThatPassesInServiceHubCanRetry, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(3, discharged) - assertEquals(1, observation) + assertHospitalCounters(3, 1) } } - @Test(timeout=300_000) - fun `external async operation that accesses serviceHub from flow directly will fail when retried`() { + @Test(timeout = 300_000) + fun `external async operation that accesses serviceHub from flow directly will fail when retried`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() @@ -158,16 +137,14 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() { alice.rpc.startFlow( ::FlowWithExternalOperationThatDirectlyAccessesServiceHubFailsRetry, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) } - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(1, discharged) - assertEquals(0, observation) + assertHospitalCounters(1, 0) } } - @Test(timeout=300_000) - fun `vault can be queried`() { + @Test(timeout = 300_000) + fun `vault can be queried`() { driver( DriverParameters( cordappsForAllNodes = cordappsForPackages(DummyState::class.packageName), @@ -176,64 +153,62 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() { ) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val success = alice.rpc.startFlow(::FlowWithWithExternalOperationThatQueriesVault) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) assertTrue(success) } } - @Test(timeout=300_000) - fun `data can be persisted to node database via entity manager`() { + @Test(timeout = 300_000) + fun `data can be persisted to node database via entity manager`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaEntityManager) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) assertTrue(success) } } - @Test(timeout=300_000) - fun `data can be persisted to node database via jdbc session`() { + @Test(timeout = 300_000) + fun `data can be persisted to node database via jdbc session`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaJdbcSession) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) assertTrue(success) } } - @Test(timeout=300_000) - fun `data can be persisted to node database via servicehub database transaction`() { + @Test(timeout = 300_000) + fun `data can be persisted to node database via servicehub database transaction`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaDatabaseTransaction) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) assertTrue(success) } } - @Test(timeout=300_000) - fun `data can be persisted to node database in external operation and read from another process once finished`() { + @Test(timeout = 300_000) + fun `data can be persisted to node database in external operation and read from another process once finished`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsToDatabaseAndReadsFromExternalOperation) - .returnValue.getOrThrow(20.seconds) + .returnValue.getOrThrow(1.minutes) assertTrue(success) } } - @Test(timeout=300_000) - fun `external operation can be retried when an error occurs inside of database transaction`() { + @Test(timeout = 300_000) + fun `external operation can be retried when an error occurs inside of database transaction`() { driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val bob = startNode(providedName = BOB_NAME).getOrThrow() val success = alice.rpc.startFlow( ::FlowWithExternalOperationThatErrorsInsideOfDatabaseTransaction, bob.nodeInfo.singleIdentity() - ).returnValue.getOrThrow(20.seconds) + ).returnValue.getOrThrow(1.minutes) assertTrue(success as Boolean) - val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow() - assertEquals(1, discharged) - assertEquals(0, observation) + assertHospitalCounters(1, 0) } } From 29a36c6b4f17a00d4970492ead4f298897e3304f Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 18 Mar 2020 13:59:10 +0000 Subject: [PATCH 19/22] Prevent Quasar from instrumenting classes that belong to AttachmentsClassLoader. (#6077) --- node/build.gradle | 3 ++- node/capsule/build.gradle | 2 +- .../kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/node/build.gradle b/node/build.gradle index ec7c5df061..d1d7f59944 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -270,7 +270,8 @@ task slowIntegrationTest(type: Test) { // quasar exclusions upon agent code instrumentation at run-time quasar { excludeClassLoaders.addAll( - 'net.corda.djvm.**' + 'net.corda.djvm.**', + 'net.corda.core.serialization.internal.**' ) excludePackages.addAll( "antlr**", diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 6d0ff63cdb..75adfe6a02 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -92,7 +92,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [ applicationId = "net.corda.node.Corda" // See experimental/quasar-hook/README.md for how to generate. def quasarExcludeExpression = "x(antlr**;bftsmart**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;kotlin**;net.corda.djvm**;djvm**;net.bytebuddy**;net.i2p**;org.apache**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**)" - def quasarClassLoaderExclusion = "l(net.corda.djvm.**)" + def quasarClassLoaderExclusion = "l(net.corda.djvm.**;net.corda.core.serialization.internal.**)" javaAgents = quasar_classifier == null ? ["quasar-core-${quasar_version}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"] : ["quasar-core-${quasar_version}-${quasar_classifier}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"] systemProperties['visualvm.display.name'] = 'Corda' if (JavaVersion.current() == JavaVersion.VERSION_1_8) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 8c2b288f05..4ab4d7ba07 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -918,7 +918,7 @@ class DriverDSLImpl( "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;" + "com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;)" - val excludeClassloaderPattern = "l(net.corda.djvm.**)" + val excludeClassloaderPattern = "l(net.corda.djvm.**;net.corda.core.serialization.internal.**)" val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath=$excludePackagePattern$excludeClassloaderPattern" From 56067acd208b1b58f17cca85c0eb40ad0e9a1ff5 Mon Sep 17 00:00:00 2001 From: Dimos Raptis Date: Wed, 18 Mar 2020 14:37:04 +0000 Subject: [PATCH 20/22] [CORDA-3628] - Remove overloads for sendAll (#6078) --- core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt | 4 ++-- docs/source/api-flows.rst | 2 +- .../kotlin/net/corda/node/services/messaging/Messaging.kt | 2 +- .../net/corda/node/services/messaging/MessagingExecutor.kt | 2 +- .../net/corda/node/services/messaging/P2PMessagingClient.kt | 2 +- .../net/corda/node/services/statemachine/FlowMessaging.kt | 2 +- .../node/services/statemachine/FlowParallelMessagingTests.kt | 2 +- .../corda/testing/node/internal/MockNodeMessagingService.kt | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 6a45d1b0b6..808c68fbe7 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -333,7 +333,7 @@ abstract class FlowLogic { @JvmOverloads fun sendAll(payload: Any, sessions: Set, maySkipCheckpoint: Boolean = false) { val sessionToPayload = sessions.map { it to payload }.toMap() - return sendAll(sessionToPayload, maySkipCheckpoint) + return sendAllMap(sessionToPayload, maySkipCheckpoint) } /** @@ -348,7 +348,7 @@ abstract class FlowLogic { */ @Suspendable @JvmOverloads - fun sendAll(payloadsPerSession: Map, maySkipCheckpoint: Boolean = false) { + fun sendAllMap(payloadsPerSession: Map, maySkipCheckpoint: Boolean = false) { val request = FlowIORequest.Send( sessionToMessage = serializePayloads(payloadsPerSession) ) diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 257690b290..05d6033c74 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -272,7 +272,7 @@ In addition ``FlowLogic`` provides functions that can receive messages from mult * Receives from all ``FlowSession`` objects specified in the passed in list. The received types must be the same. * ``sendAll(payload: Any, sessions: Set)`` * Sends the ``payload`` object to all the provided ``FlowSession``\s. -* ``sendAll(payloadsPerSession: Map)`` +* ``sendAllMap(payloadsPerSession: Map)`` * Sends a potentially different payload to each ``FlowSession``, as specified by the provided ``payloadsPerSession``. .. note:: It's more efficient to call ``sendAndReceive`` instead of calling ``send`` and then ``receive``. It's also more efficient to call ``sendAll``/``receiveAll`` instead of multiple ``send``/``receive`` respectively. diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index a9be394ac9..d2f440ef93 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -91,7 +91,7 @@ interface MessagingService : ServiceLifecycleSupport { * @param addressedMessages The list of messages together with the recipients, retry ids and sequence keys. */ @Suspendable - fun send(addressedMessages: List) + fun sendAll(addressedMessages: List) /** * Returns an initialised [Message] with the current time, etc, already filled in. 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 96a75c6e59..eead9f5698 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 @@ -54,7 +54,7 @@ class MessagingExecutor( } @Synchronized - fun send(messages: Map) { + fun sendAll(messages: Map) { messages.forEach { recipients, message -> send(message, recipients) } } 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 8254c0138f..0fcb7a3ca7 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 @@ -511,7 +511,7 @@ class P2PMessagingClient(val config: NodeConfiguration, } @Suspendable - override fun send(addressedMessages: List) { + override fun sendAll(addressedMessages: List) { for ((message, target, sequenceKey) in addressedMessages) { send(message, target, sequenceKey) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowMessaging.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowMessaging.kt index da371d6d25..24a6604ffc 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowMessaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowMessaging.kt @@ -64,7 +64,7 @@ class FlowMessagingImpl(val serviceHub: ServiceHubInternal): FlowMessaging { @Suspendable override fun sendSessionMessages(messageData: List) { val addressedMessages = messageData.map { createMessage(it.destination, it.sessionMessage, it.dedupId) } - serviceHub.networkService.send(addressedMessages) + serviceHub.networkService.sendAll(addressedMessages) } private fun createMessage(destination: Destination, message: SessionMessage, deduplicationId: SenderDeduplicationId): MessagingService.AddressedMessage { diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowParallelMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowParallelMessagingTests.kt index 53f7d588ea..fd0e161255 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowParallelMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowParallelMessagingTests.kt @@ -165,7 +165,7 @@ class FlowParallelMessagingTests { Pair(session, messageType) }.toMap() - sendAll(messagesPerSession) + sendAllMap(messagesPerSession) val messages = receiveAll(String::class.java, messagesPerSession.keys.toList()) messages.map { it.unwrap { payload -> assertEquals("pong", payload) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNodeMessagingService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNodeMessagingService.kt index ac8b2606bc..3b81fbc2ef 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNodeMessagingService.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockNodeMessagingService.kt @@ -161,7 +161,7 @@ class MockNodeMessagingService(private val configuration: NodeConfiguration, } } - override fun send(addressedMessages: List) { + override fun sendAll(addressedMessages: List) { for ((message, target, sequenceKey) in addressedMessages) { send(message, target, sequenceKey) } From dd7852f2b8438b769117cd78443892ebcd5d1c92 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 19 Mar 2020 11:20:53 +0000 Subject: [PATCH 21/22] CORDA-3668: Prevent AttachmentURLConnection from assigning ALL_PERMISSIONS. (#6080) * CORDA-3668: Prevent AttachmentURLConnection from assigning ALL_PERMISSIONS to classes inside an attachment. * Strengthen the comment warning about AttachmentURLConnection.getPermission. --- .../AttachmentsClassLoaderTests.kt | 89 ++++++++++++------- .../internal/AttachmentsClassLoader.kt | 10 +++ 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index 29edf8938e..6a98b20172 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -21,14 +21,18 @@ import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils +import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.URL import kotlin.test.assertFailsWith +import kotlin.test.fail class AttachmentsClassLoaderTests { companion object { @@ -84,14 +88,29 @@ class AttachmentsClassLoaderTests { } } + @Test(timeout=300_000) + fun `test contracts have no permissions for protection domain`() { + val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") + assertNull(System.getSecurityManager()) + + createClassloader(isolatedId).use { classLoader -> + val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader) + val protectionDomain = contractClass.protectionDomain ?: fail("Protection Domain missing") + val permissions = protectionDomain.permissions ?: fail("Protection domain has no permissions") + assertThat(permissions.elements().toList()).isEmpty() + assertTrue(permissions.isReadOnly) + } + } + @Test(timeout=300_000) fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() { val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") - val classloader = createClassloader(isolatedId) - val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader) - val contract = contractClass.getDeclaredConstructor().newInstance() as Contract - assertEquals("helloworld", contract.declaredField("magicString").value) + createClassloader(isolatedId).use { classloader -> + val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader) + val contract = contractClass.getDeclaredConstructor().newInstance() as Contract + assertEquals("helloworld", contract.declaredField("magicString").value) + } } @Test(timeout=300_000) @@ -100,7 +119,7 @@ class AttachmentsClassLoaderTests { val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar") assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) { - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } } @@ -111,7 +130,7 @@ class AttachmentsClassLoaderTests { val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar") // does not throw OverlappingAttachments exception - createClassloader(listOf(isolatedId, isolatedSignedId)) + createClassloader(listOf(isolatedId, isolatedSignedId)).use {} } @Test(timeout=300_000) @@ -120,7 +139,7 @@ class AttachmentsClassLoaderTests { val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar") // does not throw OverlappingAttachments exception - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } @Test(timeout=300_000) @@ -128,12 +147,13 @@ class AttachmentsClassLoaderTests { val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar") val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar") - val cl = createClassloader(listOf(att1, att2)) - val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) - assertEquals("some data", txt) + createClassloader(listOf(att1, att2)).use { cl -> + val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) + assertEquals("some data", txt) - val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name()) - assertEquals("some other data", txt1) + val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name()) + assertEquals("some other data", txt1) + } } @Test(timeout=300_000) @@ -141,9 +161,10 @@ class AttachmentsClassLoaderTests { val att1 = importAttachment(fakeAttachment("file1.txt", "same data", "file2.txt", "same other data").inputStream(), "app", "file1.jar") val att2 = importAttachment(fakeAttachment("file1.txt", "same data", "file3.txt", "same totally different").inputStream(), "app", "file2.jar") - val cl = createClassloader(listOf(att1, att2)) - val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) - assertEquals("same data", txt) + createClassloader(listOf(att1, att2)).use { cl -> + val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) + assertEquals("same data", txt) + } } @Test(timeout=300_000) @@ -152,7 +173,7 @@ class AttachmentsClassLoaderTests { val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar") val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar") - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } } @@ -161,7 +182,7 @@ class AttachmentsClassLoaderTests { val att1 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some data").inputStream(), "app", "file1.jar") val att2 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some other data").inputStream(), "app", "file2.jar") - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } @Test(timeout=300_000) @@ -170,7 +191,7 @@ class AttachmentsClassLoaderTests { val att2 = importAttachment(fakeAttachment("meta-inf/services/com.example.something", "some other data").inputStream(), "app", "file2.jar") assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) { - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } } @@ -180,7 +201,7 @@ class AttachmentsClassLoaderTests { val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar") assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) { - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } } @@ -191,7 +212,7 @@ class AttachmentsClassLoaderTests { val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", ISOLATED_CONTRACTS_JAR_PATH.file) val att2 = importAttachment(fakeAttachment("net/corda/finance/contracts/isolated/AnotherDummyContract\$State.class", "some attackdata").inputStream(), "app", "file2.jar") assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) { - createClassloader(listOf(att1, att2)) + createClassloader(listOf(att1, att2)).use {} } } @@ -220,10 +241,10 @@ class AttachmentsClassLoaderTests { val untrustedClassJar = importAttachment(fakeAttachment("/com/example/something/MaliciousClass.class", "some malicious data").inputStream(), "untrusted", "file2.jar") val trustedClassJar = importAttachment(fakeAttachment("/com/example/something/VirtuousClass.class", "some other data").inputStream(), "app", "file3.jar") - createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar)) - - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar)) + createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar)).use { + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar)).use {} + } } } @@ -257,7 +278,7 @@ class AttachmentsClassLoaderTests { signers = listOf(keyPairA.public, keyPairB.public) ) - createClassloader(untrustedAttachment) + createClassloader(untrustedAttachment).use {} } @Test(timeout=300_000) @@ -287,7 +308,7 @@ class AttachmentsClassLoaderTests { signers = listOf(keyPairA.public, keyPairB.public) ) - createClassloader(untrustedAttachment) + createClassloader(untrustedAttachment).use {} } @Test(timeout=300_000) @@ -306,7 +327,7 @@ class AttachmentsClassLoaderTests { ) assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment) + createClassloader(untrustedAttachment).use {} } } @@ -337,7 +358,7 @@ class AttachmentsClassLoaderTests { ) assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment) + createClassloader(untrustedAttachment).use {} } } @@ -380,10 +401,10 @@ class AttachmentsClassLoaderTests { ) // pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment - createClassloader(inheritedTrustAttachment) - - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment) + createClassloader(inheritedTrustAttachment).use { + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } } } @@ -421,7 +442,7 @@ class AttachmentsClassLoaderTests { ) assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment) + createClassloader(untrustedAttachment).use {} } } @@ -446,6 +467,6 @@ class AttachmentsClassLoaderTests { signers = listOf(keyPairA.public) ) - createClassloader(trustedAttachment) + createClassloader(trustedAttachment).use {} } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index e195a46064..5188ae1eff 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -18,6 +18,7 @@ import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream import java.net.* +import java.security.Permission import java.util.* /** @@ -378,6 +379,15 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory { private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) { override fun getContentLengthLong(): Long = attachment.size.toLong() override fun getInputStream(): InputStream = attachment.open() + /** + * Define the permissions that [AttachmentsClassLoader] will need to + * use this [URL]. The attachment is stored in memory, and so we + * don't need any extra permissions here. But if we don't override + * [getPermission] then [AttachmentsClassLoader] will assign the + * default permission of ALL_PERMISSION to these classes' + * [java.security.ProtectionDomain]. This would be a security hole! + */ + override fun getPermission(): Permission? = null override fun connect() { connected = true } From 82d999571775eff0202d542206e53c4285bb8595 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Thu, 19 Mar 2020 15:43:08 +0000 Subject: [PATCH 22/22] ENT-5109 Harmonize config-common, make everything compile again and harmonize NetworkParameterOverridesSpec. (#6082) --- .../parsing/internal/Configuration.kt | 110 ++++++++------- .../parsing/internal/Properties.kt | 107 +++++++------- .../configuration/parsing/internal/Schema.kt | 6 +- .../configuration/parsing/internal/Utils.kt | 15 +- .../internal/versioned/VersionExtractor.kt | 8 +- .../parsing/internal/PropertyTest.kt | 32 ++--- .../internal/PropertyValidationTest.kt | 36 ++--- .../parsing/internal/SchemaTest.kt | 16 +-- .../parsing/internal/SpecificationTest.kt | 18 +-- .../parsing/internal/TestUtils.kt | 2 +- .../network/NetworkParameterOverridesSpec.kt | 30 ++-- .../net/corda/node/NodeCmdLineOptions.kt | 2 +- .../subcommands/ValidateConfigurationCli.kt | 2 +- .../node/services/config/ConfigUtilities.kt | 2 +- .../node/services/config/NodeConfiguration.kt | 2 +- .../config/schema/v1/ConfigSections.kt | 131 ++++++++++-------- .../schema/v1/V1NodeConfigurationSpec.kt | 109 +++++++-------- 17 files changed, 326 insertions(+), 302 deletions(-) diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt index 7883f82586..ba623b2b56 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt @@ -21,7 +21,7 @@ object Configuration { /** * Describes a [Config] hiding sensitive data. */ - fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }): ConfigValue? + fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }, options: Options): ConfigValue? } object Value { @@ -36,10 +36,11 @@ object Configuration { * * @throws ConfigException.Missing if the [Config] does not specify the value. * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type. - * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules.. + * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to + * application-level validation rules. */ @Throws(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class) - fun valueIn(configuration: Config): TYPE + fun valueIn(configuration: Config, options: Options): TYPE /** * Returns whether the value is specified by the [Config]. @@ -50,27 +51,28 @@ object Configuration { * Returns a value out of a [Config] if all is good, or null if no value is present. Otherwise, it throws an exception. * * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type. - * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules.. + * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to + * application-level validation rules. */ @Throws(ConfigException.WrongType::class, ConfigException.BadValue::class) - fun valueInOrNull(configuration: Config): TYPE? { + fun valueInOrNull(configuration: Config, options: Options): TYPE? { return when { - isSpecifiedBy(configuration) -> valueIn(configuration) + isSpecifiedBy(configuration) -> valueIn(configuration, options) else -> null } } } /** - * Able to parse a value from a [Config] and [Configuration.Validation.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s. + * Able to parse a value from a [Config] and [Configuration.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s. */ interface Parser { /** * Returns a [Valid] wrapper either around a valid value extracted from the [Config], or around a set of [Configuration.Validation.Error] with details about what went wrong. */ - fun parse(configuration: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults): Valid + fun parse(configuration: Config, options: Options = Options.defaults): Valid } } @@ -109,11 +111,6 @@ object Configuration { */ interface Definition : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor, Configuration.Describer, Configuration.Value.Parser { - /** - * Validates target [Config] with default [Configuration.Validation.Options]. - */ - fun validate(target: Config): Valid = validate(target, Configuration.Validation.Options.defaults) - override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key) /** @@ -181,9 +178,8 @@ object Configuration { fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } } - override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated { - - return validate(configuration, options).mapValid { config -> valid(valueIn(config)) } + override fun parse(configuration: Config, options: Configuration.Options): Validated { + return validate(configuration, options).mapValid { config -> valid(valueIn(config, options)) } } companion object { @@ -199,7 +195,6 @@ object Configuration { * Returns a [Configuration.Property.Definition.Standard] with value of type [Int]. */ fun int(key: String, sensitive: Boolean = false): Standard = long(key, sensitive).mapValid { value -> - try { valid(Math.toIntExact(value)) } catch (e: ArithmeticException) { @@ -210,18 +205,17 @@ object Configuration { /** * Returns a [Configuration.Property.Definition.Standard] with value of type [Boolean]. */ - fun boolean(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Boolean::class.javaObjectType.simpleName, Config::getBoolean, Config::getBooleanList, sensitive) + fun boolean(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Boolean::class.javaObjectType.simpleName, { config, path, _ -> config.getBoolean(path) }, { config, path, _ -> config.getBooleanList(path) }, sensitive) /** * Returns a [Configuration.Property.Definition.Standard] with value of type [Double]. */ - fun double(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Double::class.javaObjectType.simpleName, Config::getDouble, Config::getDoubleList, sensitive) + fun double(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Double::class.javaObjectType.simpleName, { config, path, _ -> config.getDouble(path) }, { config, path, _ -> config.getDoubleList(path) }, sensitive) /** * Returns a [Configuration.Property.Definition.Standard] with value of type [Float]. */ fun float(key: String, sensitive: Boolean = false): Standard = double(key, sensitive).mapValid { value -> - val floatValue = value.toFloat() if (floatValue.isInfinite() || floatValue.isNaN()) { invalid(Configuration.Validation.Error.BadValue.of(key, Float::class.javaObjectType.simpleName, "Provided value exceeds Float range.")) @@ -233,24 +227,43 @@ object Configuration { /** * Returns a [Configuration.Property.Definition.Standard] with value of type [String]. */ - fun string(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, String::class.java.simpleName, Config::getString, Config::getStringList, sensitive) + fun string(key: String, sensitive: Boolean = false): Standard = StandardProperty( + key, + String::class.java.simpleName, + { config, path, _ -> config.getString(path) }, + { config, path, _ -> config.getStringList(path) }, + sensitive + ) /** * Returns a [Configuration.Property.Definition.Standard] with value of type [Duration]. */ - fun duration(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Duration::class.java.simpleName, Config::getDuration, Config::getDurationList, sensitive) + fun duration(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Duration::class.java.simpleName, { config, path, _ -> config.getDuration(path) }, { config, path, _ -> config.getDurationList(path) }, sensitive) /** * Returns a [Configuration.Property.Definition.Standard] with value of type [ConfigObject]. * It supports an optional [Configuration.Schema], which is used for validation and more when provided. */ - fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard = StandardProperty(key, ConfigObject::class.java.simpleName, Config::getObject, Config::getObjectList, sensitive, schema) + fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard = StandardProperty( + key, + ConfigObject::class.java.simpleName, + { config, path, _ -> config.getObject(path) }, + { config, path, _ -> config.getObjectList(path) }, + sensitive, + schema + ) /** * Returns a [Configuration.Property.Definition.Standard] with value of type [ENUM]. * This property expects a value in the configuration matching one of the cases of [ENUM], as text, in uppercase. */ - fun > enum(key: String, enumClass: KClass, sensitive: Boolean = false): Standard = StandardProperty(key, enumClass.java.simpleName, { conf: Config, propertyKey: String -> conf.getEnum(enumClass.java, propertyKey) }, { conf: Config, propertyKey: String -> conf.getEnumList(enumClass.java, propertyKey) }, sensitive) + fun > enum(key: String, enumClass: KClass, sensitive: Boolean = false): Standard = StandardProperty( + key, + enumClass.java.simpleName, + { conf: Config, propertyKey: String, _ -> conf.getEnum(enumClass.java, propertyKey) }, + { conf: Config, propertyKey: String, _ -> conf.getEnumList(enumClass.java, propertyKey) }, + sensitive + ) } } } @@ -275,12 +288,7 @@ object Configuration { */ val properties: Set> - /** - * Validates target [Config] with default [Configuration.Validation.Options]. - */ - fun validate(target: Config): Valid = validate(target, Configuration.Validation.Options.defaults) - - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue companion object { @@ -368,35 +376,35 @@ object Configuration { override fun description() = schema.description() - override fun validate(target: Config, options: Validation.Options) = schema.validate(target, options) + override fun validate(target: Config, options: Options) = schema.validate(target, options) - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = schema.describe(configuration, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = schema.describe(configuration, serialiseValue, options) - final override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = validate(configuration, options).mapValid(::parseValid) + final override fun parse(configuration: Config, options: Options): Valid = validate(configuration, options).mapValid { parseValid(it, options) } /** * Implement to define further mapping and validation logic, assuming the underlying raw [Config] is correct in terms of this [Configuration.Specification]. */ - protected abstract fun parseValid(configuration: Config): Valid + protected abstract fun parseValid(configuration: Config, options: Options): Valid + } + + /** + * Validation and processing options. + * @property strict whether to raise unknown property keys as errors. + */ + class Options(val strict: Boolean = false) { + + companion object { + + /** + * Default [Config] options, without [strict] parsing enabled. + */ + val defaults: Configuration.Options = Options() + } } object Validation { - /** - * [Config] validation options. - * @property strict whether to raise unknown property keys as errors. - */ - data class Options(val strict: Boolean) { - - companion object { - - /** - * Default [Config] validation options, without [strict] parsing enabled. - */ - val defaults: Configuration.Validation.Options = Options(strict = false) - } - } - /** * Super-type for the errors raised by the parsing and validation of a [Config] object. * @@ -531,7 +539,7 @@ object Configuration { } /** - * Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Validation.Options.strict] was enabled. + * Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Options.strict] was enabled. */ class Unknown private constructor(override val keyName: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, null, message(keyName), containingPath) { @@ -586,5 +594,5 @@ object Configuration { /** * Defines the ability to validate a [Config] object, producing a valid [Config] or a set of [Configuration.Validation.Error]. */ - interface Validator : net.corda.common.validation.internal.Validator + interface Validator : net.corda.common.validation.internal.Validator } \ No newline at end of file diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt index 4ce49fc5e6..048492240a 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt @@ -5,10 +5,9 @@ import net.corda.common.validation.internal.Validated import net.corda.common.validation.internal.Validated.Companion.invalid import net.corda.common.validation.internal.Validated.Companion.valid -internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) { - - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { +internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty(key, Long::class.javaObjectType.simpleName, { config, path, _ -> config.getLong(path) }, { config, path, _ -> config.getLongList(path) }, sensitive) { + override fun validate(target: Config, options: Configuration.Options): Valid { val validated = super.validate(target, options) if (validated.isValid && target.getValue(key).unwrapped().toString().contains(".")) { return invalid(ConfigException.WrongType(target.origin(), key, Long::class.javaObjectType.simpleName, Double::class.javaObjectType.simpleName).toValidationError(key, typeName)) @@ -17,9 +16,11 @@ internal class LongProperty(key: String, sensitive: Boolean = false) : StandardP } } -internal open class StandardProperty(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard { +typealias ValueSelector = (Config, String, Configuration.Options) -> T - override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key) +internal open class StandardProperty(override val key: String, typeNameArg: String, private val extractSingleValue: ValueSelector, internal val extractListValue: ValueSelector>, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard { + + override fun valueIn(configuration: Config, options: Configuration.Options) = extractSingleValue.invoke(configuration, key, options) override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg @@ -29,20 +30,18 @@ internal open class StandardProperty(override val key: String, typeN override fun list(): Configuration.Property.Definition.RequiredList = ListProperty(this) - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { - + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) } - return schema?.describe(configuration.getConfig(key), serialiseValue) ?: valueDescription(valueIn(configuration), serialiseValue) + return schema?.describe(configuration.getConfig(key), serialiseValue, options) ?: valueDescription(valueIn(configuration, options), serialiseValue) } override val isMandatory = true - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { - + override fun validate(target: Config, options: Configuration.Options): Valid { val errors = mutableSetOf() - errors += errorsWhenExtractingValue(target) + errors += errorsWhenExtractingValue(target, options) if (errors.isEmpty()) { schema?.let { nestedSchema -> val nestedConfig: Config? = target.getConfig(key) @@ -61,15 +60,19 @@ private class ListProperty(delegate: StandardProperty) : Requi override val typeName: String = "List<${delegate.typeName}>" - override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key) - - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { + override fun valueIn(configuration: Config, options: Configuration.Options): List = delegate.extractListValue.invoke(configuration, key, options) + override fun validate(target: Config, options: Configuration.Options): Valid { val errors = mutableSetOf() - errors += errorsWhenExtractingValue(target) + errors += errorsWhenExtractingValue(target, options) if (errors.isEmpty()) { delegate.schema?.let { schema -> - errors += valueIn(target).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }.fold(emptyList()) { one, other -> one + other }.toSet() + errors += valueIn(target, options).asSequence() + .map { element -> element as ConfigObject } + .map(ConfigObject::toConfig) + .mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } } + .fold(emptyList()) { one, other -> one + other } + .toSet() } } return Validated.withResult(target, errors) @@ -77,17 +80,16 @@ private class ListProperty(delegate: StandardProperty) : Requi override fun mapValid(mappedTypeName: String, convert: (List) -> Validated): Configuration.Property.Definition.Required = ListMappingProperty(this, mappedTypeName, convert) - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { - + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) } return when { delegate.schema != null -> { - val elementsDescription = valueIn(configuration).asSequence().map { it as ConfigObject }.map(ConfigObject::toConfig).map { delegate.schema.describe(it, serialiseValue) }.toList() + val elementsDescription = valueIn(configuration, options).asSequence().map { it as ConfigObject }.map(ConfigObject::toConfig).map { delegate.schema.describe(it, serialiseValue, options) }.toList() ConfigValueFactory.fromIterable(elementsDescription) } - else -> valueDescription(valueIn(configuration), serialiseValue) + else -> valueDescription(valueIn(configuration, options), serialiseValue) } } @@ -106,16 +108,17 @@ private class OptionalPropertyWithDefault(delegate: Configuration.Property override val typeName: String = delegate.typeName.removeSuffix("?") - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue? = delegate.describe(configuration, serialiseValue, options) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue) - override fun valueIn(configuration: Config): TYPE = delegate.valueIn(configuration) ?: defaultValue + override fun valueIn(configuration: Config, options: Configuration.Options): TYPE = delegate.valueIn(configuration, options) ?: defaultValue - override fun validate(target: Config, options: Configuration.Validation.Options): Valid = delegate.validate(target, options) + override fun validate(target: Config, options: Configuration.Options): Valid = delegate.validate(target, options) } -private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List, private val convert: (TYPE) -> Valid) : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard { +private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: ValueSelector>, private val convert: (TYPE) -> Valid) + : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard { - override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value() + override fun valueIn(configuration: Config, options: Configuration.Options) = convert.invoke(delegate.valueIn(configuration, options)).value() override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})" @@ -123,29 +126,31 @@ private class FunctionalProperty(delegate: Configuration.Property. override fun list(): Configuration.Property.Definition.RequiredList = FunctionalListProperty(this) - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { - + override fun validate(target: Config, options: Configuration.Options): Valid { val errors = mutableSetOf() errors += delegate.validate(target, options).errors if (errors.isEmpty()) { - errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors + errors += convert.invoke(delegate.valueIn(target, options)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors } return Validated.withResult(target, errors) } - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = delegate.describe(configuration, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = delegate.describe(configuration, serialiseValue, options) } private class FunctionalListProperty(delegate: FunctionalProperty) : RequiredDelegatedProperty, FunctionalProperty>(delegate), Configuration.Property.Definition.RequiredList { override val typeName: String = "List<${super.typeName}>" - override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key).asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.map(ConfigObject::toConfig).map(delegate::valueIn).toList() - - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { + override fun valueIn(configuration: Config, options: Configuration.Options): List = delegate.extractListValue.invoke(configuration, key, options).asSequence() + .map { configObject(key to ConfigValueFactory.fromAnyRef(it)) } + .map(ConfigObject::toConfig) + .map { delegate.valueIn(it, options) } + .toList() + override fun validate(target: Config, options: Configuration.Options): Valid { val list = try { - delegate.extractListValue.invoke(target, key) + delegate.extractListValue.invoke(target, key, options) } catch (e: ConfigException) { if (isErrorExpected(e)) { return invalid(e.toValidationError(key, typeName)) @@ -153,7 +158,11 @@ private class FunctionalListProperty(delegate: FunctionalProperty delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }.fold(emptyList()) { one, other -> one + other }.toSet() + val errors = list.asSequence() + .map { configObject(key to ConfigValueFactory.fromAnyRef(it)) } + .mapIndexed { index, value -> delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } } + .fold(emptyList()) { one, other -> one + other } + .toSet() return Validated.withResult(target, errors) } @@ -165,12 +174,11 @@ private class FunctionalListProperty(delegate: FunctionalProperty ConfigValue): ConfigValue { - + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) } - return delegate.schema?.let { schema -> valueDescription(valueIn(configuration).asSequence().map { element -> valueDescription(element, serialiseValue) }.map { it as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it, serialiseValue) }.toList(), serialiseValue) } ?: valueDescription(valueIn(configuration), serialiseValue) + return delegate.schema?.let { schema -> valueDescription(valueIn(configuration, options).asSequence() .map { element -> valueDescription(element, serialiseValue) } .map { it as ConfigObject } .map(ConfigObject::toConfig) .map { schema.describe(it, serialiseValue, options) } .toList(), serialiseValue) } ?: valueDescription(valueIn(configuration, options), serialiseValue) } override fun mapValid(mappedTypeName: String, convert: (List) -> Validated): Configuration.Property.Definition.Required = ListMappingProperty(this, mappedTypeName, convert) @@ -187,18 +195,16 @@ private class OptionalDelegatedProperty(private val delegate: Configuratio override val typeName: String = "${delegate.typeName}?" - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue) else null - - override fun valueIn(configuration: Config): TYPE? { + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue, options) else null + override fun valueIn(configuration: Config, options: Configuration.Options): TYPE? { return when { - isSpecifiedBy(configuration) -> delegate.valueIn(configuration) + isSpecifiedBy(configuration) -> delegate.valueIn(configuration, options) else -> null } } - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { - + override fun validate(target: Config, options: Configuration.Options): Valid { val result = delegate.validate(target, options) val errors = result.errors val missingValueError = errors.asSequence().filterIsInstance().filter { it.pathAsString == key }.singleOrNull() @@ -221,18 +227,17 @@ private abstract class RequiredDelegatedProperty(private val delegate: Configuration.Property.Definition.RequiredList, private val mappedTypeName: String, private val convert: (List) -> Validated) : Configuration.Property.Definition.Required { - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue? = delegate.describe(configuration, serialiseValue, options) - override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value() + override fun valueIn(configuration: Config, options: Configuration.Options) = convert.invoke(delegate.valueIn(configuration, options)).value() override fun optional(): Configuration.Property.Definition.Optional = OptionalDelegatedProperty(this) - override fun validate(target: Config, options: Configuration.Validation.Options): Validated { - + override fun validate(target: Config, options: Configuration.Options): Validated { val errors = mutableSetOf() errors += delegate.validate(target, options).errors if (errors.isEmpty()) { - errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors + errors += convert.invoke(delegate.valueIn(target, options)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors } return Validated.withResult(target, errors) } @@ -248,7 +253,6 @@ private class ListMappingProperty(private val delegate: Configurat } fun ConfigException.toValidationError(keyName: String? = null, typeName: String): Configuration.Validation.Error { - val toError = when (this) { is ConfigException.Missing -> Configuration.Validation.Error.MissingValue.Companion::of is ConfigException.WrongType -> Configuration.Validation.Error.WrongType.Companion::of @@ -260,10 +264,9 @@ fun ConfigException.toValidationError(keyName: String? = null, typeName: String) return toError.invoke(message!!, keyName, typeName, emptyList()) } -private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config): Set { - +private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config, options: Configuration.Options): Set { try { - valueIn(target) + valueIn(target, options) return emptySet() } catch (exception: ConfigException) { if (isErrorExpected(exception)) { diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt index 348714fef5..48b3fa78d0 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt @@ -16,7 +16,7 @@ internal class Schema(override val name: String?, unorderedProperties: Iterable< } } - override fun validate(target: Config, options: Configuration.Validation.Options): Valid { + override fun validate(target: Config, options: Configuration.Options): Valid { val propertyErrors = properties.flatMap { property -> property.validate(target, options).errors @@ -47,9 +47,9 @@ internal class Schema(override val name: String?, unorderedProperties: Iterable< return description.toString() } - override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue { - return properties.asSequence().map { it.key to it.describe(configuration, serialiseValue) }.filter { it.second != null }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) } + return properties.asSequence().map { it.key to it.describe(configuration, serialiseValue, options) }.filter { it.second != null }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) } } override fun equals(other: Any?): Boolean { diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt index e7455a38a7..e2ba999108 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt @@ -21,7 +21,11 @@ inline fun Configuration.Property.Definition.RequiredList inline fun Configuration.Property.Definition.RequiredList.map(noinline convert: (List) -> MAPPED): Configuration.Property.Definition.Required = map(MAPPED::class.java.simpleName, convert) -operator fun Config.get(property: Configuration.Property.Definition): TYPE = property.valueIn(this) +fun Config.withOptions(options: Configuration.Options) = ConfigurationWithOptions(this, options) + +data class ConfigurationWithOptions(private val config: Config, private val options: Configuration.Options) { + operator fun get(property: Configuration.Value.Extractor): TYPE = property.valueIn(config, options) +} inline fun Configuration.Specification<*>.nested(specification: Configuration.Specification, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) } @@ -66,15 +70,6 @@ internal typealias Valid = Validated valid(target: TYPE) = Validated.valid(target) -/** - * Value extracted from a configuration file is a function of the actual value specified and configuration options. - * E.g. password value may be stored in the encrypted form rather than in a clear text. - */ -data class ConfigurationWithOptions(private val config: Config, private val options: Configuration.Validation.Options) { - operator fun get(property: Configuration.Property.Definition): TYPE = property.valueIn(config) - operator fun get(property: Configuration.Value.Extractor): TYPE = property.valueIn(config) -} - /** * Helper interface to mark objects that will have [ConfigurationWithOptions] in them. */ diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt index 55e34d6467..aefe57d132 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt @@ -6,21 +6,17 @@ import net.corda.common.configuration.parsing.internal.Valid import net.corda.common.configuration.parsing.internal.valid internal class VersionExtractor(versionPath: String, versionDefaultValue: Int) : Configuration.Version.Extractor { - private val containingPath = versionPath.split(".").let { if (it.size > 1) it.subList(0, it.size - 1) else null } private val key = versionPath.split(".").last() private val spec = Spec(key, versionDefaultValue, containingPath?.joinToString(".")) - override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid { - + override fun parse(configuration: Config, options: Configuration.Options): Valid { return spec.parse(configuration) } private class Spec(key: String, versionDefaultValue: Int, prefix: String?) : Configuration.Specification("Version", prefix) { - private val version by int(key = key).optional().withDefaultValue(versionDefaultValue) - - override fun parseValid(configuration: Config) = valid(version.valueIn(configuration)) + override fun parseValid(configuration: Config, options: Configuration.Options) = valid(version.valueIn(configuration, options)) } } \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt index 61dbd39551..75a08a2f59 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt @@ -21,8 +21,8 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value) - assertThat(configuration[property]).isEqualTo(value) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value) + assertThat(configuration.withOptions(Configuration.Options.defaults)[property]).isEqualTo(value) } @Test(timeout=300_000) @@ -38,7 +38,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java) + assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.WrongType::class.java) } @Test(timeout=300_000) @@ -54,7 +54,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value) } @Test(timeout=300_000) @@ -70,7 +70,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value.max()) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value.max()) } @Test(timeout=300_000) @@ -85,7 +85,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isEqualTo(null) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(null) } @Test(timeout=300_000) @@ -101,7 +101,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value.max()) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value.max()) } @Test(timeout=300_000) @@ -116,7 +116,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isEqualTo(null) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(null) } @Test(timeout=300_000) @@ -132,7 +132,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value) } @Test(timeout=300_000) @@ -147,7 +147,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isNull() + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isNull() } @@ -164,7 +164,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isEqualTo(defaultValue) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(defaultValue) } @Test(timeout=300_000) @@ -179,7 +179,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isTrue() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.Missing::class.java) + assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.Missing::class.java) } @Test(timeout=300_000) @@ -195,7 +195,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThat(property.valueIn(configuration)).isEqualTo(value) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value) } @Test(timeout=300_000) @@ -211,7 +211,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isTrue() - assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java) + assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.WrongType::class.java) } @Test(timeout=300_000) @@ -226,7 +226,7 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isNull() + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isNull() } @Test(timeout=300_000) @@ -242,6 +242,6 @@ class PropertyTest { assertThat(property.key).isEqualTo(key) assertThat(property.isMandatory).isFalse() assertThat(property.isSpecifiedBy(configuration)).isFalse() - assertThat(property.valueIn(configuration)).isEqualTo(defaultValue) + assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(defaultValue) } } \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt index 3c03a74c33..0c748f2e70 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt @@ -15,7 +15,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key) - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -34,7 +34,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key) - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -53,7 +53,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key).list() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -72,7 +72,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key).list() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -94,7 +94,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax) - assertThat(property.validate(configuration).errors).isEmpty() + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).isEmpty() } @Test(timeout=300_000) @@ -114,7 +114,7 @@ class PropertyValidationTest { val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax) - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error -> @@ -134,7 +134,7 @@ class PropertyValidationTest { val configuration = configObject(key to false).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -154,7 +154,7 @@ class PropertyValidationTest { val configuration = configObject(key to 1.2).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -174,7 +174,7 @@ class PropertyValidationTest { val configuration = configObject(key to 1).toConfig() - assertThat(property.validate(configuration).isValid).isTrue() + assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue() } @Test(timeout=300_000) @@ -186,7 +186,7 @@ class PropertyValidationTest { val configuration = configObject(key to listOf(false, true)).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -206,7 +206,7 @@ class PropertyValidationTest { val configuration = configObject(key to listOf(1, 2, 3)).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -226,7 +226,7 @@ class PropertyValidationTest { val configuration = configObject(key to 1).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -249,7 +249,7 @@ class PropertyValidationTest { val configuration = configObject(key to configObject(nestedKey to false)).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> @@ -272,7 +272,7 @@ class PropertyValidationTest { val configuration = configObject(key to configObject()).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -295,7 +295,7 @@ class PropertyValidationTest { val configuration = configObject(key to configObject(nestedKey to null)).toConfig() - assertThat(property.validate(configuration).errors).satisfies { errors -> + assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors -> assertThat(errors).hasSize(1) assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> @@ -317,7 +317,7 @@ class PropertyValidationTest { val configuration = configObject(key to configObject(nestedKey to false)).toConfig() - assertThat(property.validate(configuration).isValid).isTrue() + assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue() } @Test(timeout=300_000) @@ -333,7 +333,7 @@ class PropertyValidationTest { val configuration = configObject(key to value).toConfig() - assertThat(property.validate(configuration).isValid).isTrue() + assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue() } @Test(timeout=300_000) @@ -350,7 +350,7 @@ class PropertyValidationTest { val configuration = configObject(key to value).toConfig() - val result = property.validate(configuration) + val result = property.validate(configuration, Configuration.Options.defaults) assertThat(result.errors).satisfies { errors -> diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt index f6393bd39b..2ad58808f2 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt @@ -29,7 +29,7 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), double("prop5")) } val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } - val result = barConfigSchema.validate(configuration) + val result = barConfigSchema.validate(configuration, Configuration.Options.defaults) println(barConfigSchema.description()) assertThat(result.isValid).isTrue() @@ -59,17 +59,17 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } - val strictErrors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = true)).errors + val strictErrors = barConfigSchema.validate(configuration, Configuration.Options(strict = true)).errors assertThat(strictErrors).hasSize(2) assertThat(strictErrors.filter { error -> error.keyName == "prop4" }).hasSize(1) assertThat(strictErrors.filter { error -> error.keyName == "prop6" }).hasSize(1) - val errors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = false)).errors + val errors = barConfigSchema.validate(configuration, Configuration.Options(strict = false)).errors assertThat(errors).isEmpty() - val errorsWithDefaultOptions = barConfigSchema.validate(configuration).errors + val errorsWithDefaultOptions = barConfigSchema.validate(configuration, Configuration.Options.defaults).errors assertThat(errorsWithDefaultOptions).isEmpty() } @@ -98,7 +98,7 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } - val result = barConfigSchema.validate(configuration) + val result = barConfigSchema.validate(configuration, Configuration.Options.defaults) assertThat(result.isValid).isTrue() } @@ -127,7 +127,7 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } - val errors = barConfigSchema.validate(configuration).errors + val errors = barConfigSchema.validate(configuration, Configuration.Options.defaults).errors errors.forEach(::println) assertThat(errors).hasSize(2) @@ -154,7 +154,7 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) } val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } - val printedConfiguration = barConfigSchema.describe(configuration) + val printedConfiguration = barConfigSchema.describe(configuration, options = Configuration.Options.defaults) val description = printedConfiguration.serialize().also { println(it) } @@ -185,7 +185,7 @@ class SchemaTest { val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) } val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema).list()) } - val printedConfiguration = barConfigSchema.describe(configuration) + val printedConfiguration = barConfigSchema.describe(configuration, options = Configuration.Options.defaults) val description = printedConfiguration.serialize().also { println(it) } diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt index a78ef5ee59..ace4e2bbe7 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt @@ -16,7 +16,7 @@ class SpecificationTest { val principal by string().mapValid(::parseAddress) val admin by string().mapValid(::parseAddress) - override fun parseValid(configuration: Config) = valid(Addresses(configuration[principal], configuration[admin])) + override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid(Addresses(it[principal], it[admin])) } private fun parseAddress(rawValue: String): Valid
{ @@ -27,7 +27,7 @@ class SpecificationTest { val useSsl by boolean() val addresses by nested(AddressesSpec) - override fun parseValid(configuration: Config) = valid(RpcSettingsImpl(configuration[addresses], configuration[useSsl])) + override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid(RpcSettingsImpl(it[addresses], it[useSsl])) } } @Test(timeout=300_000) @@ -60,9 +60,9 @@ class SpecificationTest { private val maxElement by long("elements").list().map { elements -> elements.max() } - override fun parseValid(configuration: Config): Valid { - - return valid(AtomicLong(configuration[maxElement]!!)) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(AtomicLong(config[maxElement]!!)) } } @@ -111,9 +111,9 @@ class SpecificationTest { private val maxElement by long("elements").list().mapValid(::parseMax) - override fun parseValid(configuration: Config): Valid { - - return valid(AtomicLong(configuration[maxElement])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(AtomicLong(config[maxElement])) } } @@ -159,7 +159,7 @@ class SpecificationTest { @Suppress("unused") val myProp by string().list().optional() - override fun parseValid(configuration: Config) = valid(configuration[myProp]) + override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid(it[myProp]) } } assertThat(spec.properties).hasSize(1) diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt index bf2c2f92bf..5e604fd765 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt @@ -15,5 +15,5 @@ internal fun extractValueWithErrors(errors: Set extractValue(value: Valid) = object : Configuration.Value.Parser { - override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = value + override fun parse(configuration: Config, options: Configuration.Options): Valid = value } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParameterOverridesSpec.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParameterOverridesSpec.kt index c4f159528f..ba15d49f9e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParameterOverridesSpec.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParameterOverridesSpec.kt @@ -2,9 +2,9 @@ package net.corda.nodeapi.internal.network import com.typesafe.config.Config import net.corda.common.configuration.parsing.internal.Configuration -import net.corda.common.configuration.parsing.internal.get import net.corda.common.configuration.parsing.internal.mapValid import net.corda.common.configuration.parsing.internal.nested +import net.corda.common.configuration.parsing.internal.withOptions import net.corda.common.validation.internal.Validated import net.corda.core.internal.noPackageOverlap import net.corda.core.internal.requirePackageValid @@ -17,7 +17,7 @@ import java.security.KeyStoreException typealias Valid = Validated -fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = false)): +fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Options = Configuration.Options.defaults): Valid = NetworkParameterOverridesSpec.parse(this, options) internal fun badValue(msg: String): Valid = Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of(msg)).toSet()) @@ -36,11 +36,12 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification { - val suppliedKeystorePath = configuration[keystore] - val keystorePassword = configuration[keystorePassword] + override fun parseValid(configuration: Config, options: Configuration.Options): Validated { + val config = configuration.withOptions(options) + val suppliedKeystorePath = config[keystore] + val keystorePassword = config[keystorePassword] return try { - val javaPackageName = configuration[packageName] + val javaPackageName = config[packageName] val absoluteKeystorePath = if (suppliedKeystorePath.isAbsolute) { suppliedKeystorePath } else { @@ -49,10 +50,10 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification { - val packageOwnership = configuration[packageOwnership] + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + val packageOwnership = config[packageOwnership] if (packageOwnership != null && !noPackageOverlap(packageOwnership.map { it.javaPackageName })) { return Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of( "Package namespaces must not overlap", @@ -89,11 +91,11 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification { - val option = Configuration.Validation.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL) + val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL) return configuration.parseAsNodeConfiguration(option) } diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt index 00bbdb1ffc..1024f10cff 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt @@ -26,7 +26,7 @@ internal class ValidateConfigurationCli : CliWrapperBase("validate-configuration return "for path: \"$pathAsString\": $message" } - internal fun logRawConfig(config: Config) = logger.info("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any?::toConfigValue).render(configRenderingOptions)}") + internal fun logRawConfig(config: Config) = logger.info("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any?::toConfigValue, Configuration.Options()).render(configRenderingOptions)}") } @Mixin diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 080e4e200c..b2dc5dd773 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -107,7 +107,7 @@ object ConfigHelper { newKey.let { key -> val cfg = ConfigFactory.parseMap(mapOf(key to it.value)) - val result = V1NodeConfigurationSpec.validate(cfg, Configuration.Validation.Options(strict = true)) + val result = V1NodeConfigurationSpec.validate(cfg, Configuration.Options(strict = true)) val isInvalidProperty = result.errors.any { err -> err is Configuration.Validation.Error.Unknown } if (isInvalidProperty) { 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 010e40007f..824a019df2 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 @@ -217,7 +217,7 @@ data class FlowTimeoutConfiguration( internal typealias Valid = Validated -fun Config.parseAsNodeConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = true)): Valid = V1NodeConfigurationSpec.parse(this, options) +fun Config.parseAsNodeConfiguration(options: Configuration.Options = Configuration.Options(strict = true)): Valid = V1NodeConfigurationSpec.parse(this, options) data class NodeH2Settings( val address: NetworkHostAndPort? diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt index 6e516a6bad..0132ecee53 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt @@ -5,11 +5,11 @@ package net.corda.node.services.config.schema.v1 import com.typesafe.config.Config import com.typesafe.config.ConfigObject import net.corda.common.configuration.parsing.internal.Configuration -import net.corda.common.configuration.parsing.internal.get import net.corda.common.configuration.parsing.internal.listOrEmpty import net.corda.common.configuration.parsing.internal.map import net.corda.common.configuration.parsing.internal.mapValid import net.corda.common.configuration.parsing.internal.nested +import net.corda.common.configuration.parsing.internal.withOptions import net.corda.common.validation.internal.Validated.Companion.invalid import net.corda.common.validation.internal.Validated.Companion.valid import net.corda.core.context.AuthServiceId @@ -55,11 +55,12 @@ internal object UserSpec : Configuration.Specification("User") { private val password by string(sensitive = true) private val permissions by string().listOrEmpty() - override fun parseValid(configuration: Config): Valid { - val username = configuration[username] ?: configuration[user] + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + val username = config[username] ?: config[user] return when (username) { null -> invalid(Configuration.Validation.Error.MissingValue.forKey("username")) - else -> valid(User(username, configuration[password], configuration[permissions].toSet())) + else -> valid(User(username, config[password], config[permissions].toSet())) } } } @@ -72,11 +73,12 @@ internal object SecurityConfigurationSpec : Configuration.Specification { - val type = configuration[type] - val passwordEncryption = configuration[passwordEncryption] - val connection = configuration[connection] - val users = configuration[users] + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + val type = config[type] + val passwordEncryption = config[passwordEncryption] + val connection = config[connection] + val users = config[users] return when { type == AuthDataSourceType.INMEMORY && (users == null || connection != null) -> badValue("\"INMEMORY\" data source type requires \"users\" and cannot specify \"connection\"") @@ -91,15 +93,17 @@ internal object SecurityConfigurationSpec : Configuration.Specification if (value >= 0) validValue(value) else badValue("cannot be less than 0'") } private val maxEntries by long().mapValid { value -> if (value >= 0) validValue(value) else badValue("cannot be less than 0'") } - override fun parseValid(configuration: Config): Valid { - return valid(SecurityConfiguration.AuthService.Options.Cache(configuration[expireAfterSecs], configuration[maxEntries])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(SecurityConfiguration.AuthService.Options.Cache(config[expireAfterSecs], config[maxEntries])) } } private val cache by nested(CacheSpec).optional() - override fun parseValid(configuration: Config): Valid { - return valid(SecurityConfiguration.AuthService.Options(configuration[cache])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(SecurityConfiguration.AuthService.Options(config[cache])) } } @@ -107,21 +111,23 @@ internal object SecurityConfigurationSpec : Configuration.Specification { - val dataSource = configuration[dataSource] - val id = configuration[id] ?: defaultAuthServiceId(dataSource.type) - val options = configuration[options] + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + val dataSource = config[dataSource] + val id = config[id] ?: defaultAuthServiceId(dataSource.type) + val authServiceOptions = config[this.options] return when { - dataSource.type == AuthDataSourceType.INMEMORY && options?.cache != null -> badValue("no cache supported for \"INMEMORY\" data provider") - else -> valid(SecurityConfiguration.AuthService(dataSource, id, options)) + dataSource.type == AuthDataSourceType.INMEMORY && authServiceOptions?.cache != null -> badValue("no cache supported for \"INMEMORY\" data provider") + else -> valid(SecurityConfiguration.AuthService(dataSource, id, authServiceOptions)) } } } private val authService by nested(AuthServiceSpec) - override fun parseValid(configuration: Config): Valid { - return valid(SecurityConfiguration(configuration[authService])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(SecurityConfiguration(config[authService])) } } @@ -134,13 +140,15 @@ internal object DevModeOptionsSpec : Configuration.Specification private val bootstrapSource by string().optional() private val cordaSource by string().list() - override fun parseValid(configuration: Config): Valid { - return valid(DJVMOptions(configuration[bootstrapSource], configuration[cordaSource])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(DJVMOptions(config[bootstrapSource], config[cordaSource])) } } - override fun parseValid(configuration: Config): Valid { - return valid(DevModeOptions(configuration[disableCheckpointChecker], configuration[allowCompatibilityZone], configuration[djvm])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(DevModeOptions(config[disableCheckpointChecker], config[allowCompatibilityZone], config[djvm])) } } @@ -151,8 +159,9 @@ internal object NetworkServicesConfigSpec : Configuration.Specification { - return valid(NetworkServicesConfig(configuration[doormanURL], configuration[networkMapURL], configuration[pnm], configuration[inferred], configuration[csrToken])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(NetworkServicesConfig(config[doormanURL], config[networkMapURL], config[pnm], config[inferred], config[csrToken])) } } @@ -160,22 +169,24 @@ internal object NetworkParameterAcceptanceSettingsSpec : Configuration.Specification("NetworkParameterAcceptanceSettings") { private val autoAcceptEnabled by boolean().optional().withDefaultValue(true) private val excludedAutoAcceptableParameters by string().listOrEmpty() - override fun parseValid(configuration: Config): Valid { - return valid(NetworkParameterAcceptanceSettings(configuration[autoAcceptEnabled], - configuration[excludedAutoAcceptableParameters].toSet()) + override fun parseValid(configuration: Config, options: Configuration.Options): + Valid { + val config = configuration.withOptions(options) + return valid(NetworkParameterAcceptanceSettings(config[autoAcceptEnabled], + config[excludedAutoAcceptableParameters].toSet()) ) } } - @Suppress("DEPRECATION") internal object CertChainPolicyConfigSpec : Configuration.Specification("CertChainPolicyConfig") { private val role by string() private val policy by enum(CertChainPolicyType::class) private val trustedAliases by string().listOrEmpty() - override fun parseValid(configuration: Config): Valid { - return valid(CertChainPolicyConfig(configuration[role], configuration[policy], configuration[trustedAliases].toSet())) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(CertChainPolicyConfig(config[role], config[policy], config[trustedAliases].toSet())) } } @@ -184,8 +195,9 @@ internal object FlowTimeoutConfigurationSpec : Configuration.Specification { - return valid(FlowTimeoutConfiguration(configuration[timeout], configuration[maxRestartCount], configuration[backoffBase])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(FlowTimeoutConfiguration(config[timeout], config[maxRestartCount], config[backoffBase])) } } @@ -198,8 +210,9 @@ internal object NotaryConfigSpec : Configuration.Specification("No private val raft by nested(RaftConfigSpec).optional() private val bftSMaRt by nested(BFTSmartConfigSpec).optional() - override fun parseValid(configuration: Config): Valid { - return valid(NotaryConfig(configuration[validating], configuration[serviceLegalName], configuration[className], configuration[etaMessageThresholdSeconds], configuration[extraConfig], configuration[raft], configuration[bftSMaRt])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(NotaryConfig(config[validating], config[serviceLegalName], config[className], config[etaMessageThresholdSeconds], config[extraConfig], config[raft], config[bftSMaRt])) } } @@ -207,8 +220,9 @@ internal object RaftConfigSpec : Configuration.Specification("RaftCo private val nodeAddress by string().mapValid(::toNetworkHostAndPort) private val clusterAddresses by string().mapValid(::toNetworkHostAndPort).listOrEmpty() - override fun parseValid(configuration: Config): Valid { - return valid(RaftConfig(configuration[nodeAddress], configuration[clusterAddresses])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(RaftConfig(config[nodeAddress], config[clusterAddresses])) } } @@ -218,19 +232,20 @@ internal object BFTSmartConfigSpec : Configuration.Specification private val debug by boolean().optional().withDefaultValue(false) private val exposeRaces by boolean().optional().withDefaultValue(false) - override fun parseValid(configuration: Config): Valid { - return valid(BFTSmartConfig(configuration[replicaId], configuration[clusterAddresses], configuration[debug], configuration[exposeRaces])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(BFTSmartConfig(config[replicaId], config[clusterAddresses], config[debug], config[exposeRaces])) } } - internal object NodeRpcSettingsSpec : Configuration.Specification("NodeRpcSettings") { internal object BrokerRpcSslOptionsSpec : Configuration.Specification("BrokerRpcSslOptions") { private val keyStorePath by string().mapValid(::toPath) private val keyStorePassword by string(sensitive = true) - override fun parseValid(configuration: Config): Valid { - return valid(BrokerRpcSslOptions(configuration[keyStorePath], configuration[keyStorePassword])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(BrokerRpcSslOptions(config[keyStorePath], config[keyStorePassword])) } } @@ -240,18 +255,18 @@ internal object NodeRpcSettingsSpec : Configuration.Specification { - return valid(NodeRpcSettings(configuration[address], configuration[adminAddress], configuration[standAloneBroker], configuration[useSsl], configuration[ssl])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(NodeRpcSettings(config[address], config[adminAddress], config[standAloneBroker], config[useSsl], config[ssl])) } } internal object SSHDConfigurationSpec : Configuration.Specification("SSHDConfiguration") { private val port by int() - override fun parseValid(configuration: Config): Valid = attempt { SSHDConfiguration(configuration[port]) } + override fun parseValid(configuration: Config, options: Configuration.Options): Valid = attempt { SSHDConfiguration(configuration.withOptions(options)[port]) } } - internal object DatabaseConfigSpec : Configuration.Specification("DatabaseConfig") { private val initialiseSchema by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.initialiseSchema) private val initialiseAppSchema by enum(SchemaInitializationType::class).optional().withDefaultValue(DatabaseConfig.Defaults.initialiseAppSchema) @@ -259,16 +274,18 @@ internal object DatabaseConfigSpec : Configuration.Specification private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics) private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize) - override fun parseValid(configuration: Config): Valid { - return valid(DatabaseConfig(configuration[initialiseSchema], configuration[initialiseAppSchema], configuration[transactionIsolationLevel], configuration[exportHibernateJMXStatistics], configuration[mappedSchemaCacheSize])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(DatabaseConfig(config[initialiseSchema], config[initialiseAppSchema], config[transactionIsolationLevel], config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize])) } } internal object NodeH2SettingsSpec : Configuration.Specification("NodeH2Settings") { private val address by string().mapValid(::toNetworkHostAndPort).optional() - override fun parseValid(configuration: Config): Valid { - return valid(NodeH2Settings(configuration[address])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(NodeH2Settings(config[address])) } } @@ -277,14 +294,16 @@ internal object FlowOverridesConfigSpec : Configuration.Specification { - return valid(FlowOverride(configuration[initiator], configuration[responder])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(FlowOverride(config[initiator], config[responder])) } } private val overrides by nested(FlowOverridesConfigSpec.SingleSpec).listOrEmpty() - override fun parseValid(configuration: Config): Valid { - return valid(FlowOverrideConfig(configuration[overrides])) + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(FlowOverrideConfig(config[overrides])) } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt index 7f7b66bc9f..084faa1545 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt @@ -70,67 +70,68 @@ internal object V1NodeConfigurationSpec : Configuration.Specification { + override fun parseValid(configuration: Config, options: Configuration.Options): Validated { + val config = configuration.withOptions(options) - val messagingServerExternal = configuration[messagingServerExternal] ?: Defaults.messagingServerExternal(configuration[messagingServerAddress]) - val database = configuration[database] ?: Defaults.database(configuration[devMode]) - val baseDirectoryPath = configuration[baseDirectory] - val cordappDirectories = configuration[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath) + val messagingServerExternal = config[messagingServerExternal] ?: Defaults.messagingServerExternal(config[messagingServerAddress]) + val database = config[database] ?: Defaults.database(config[devMode]) + val baseDirectoryPath = config[baseDirectory] + val cordappDirectories = config[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath) val result = try { valid(NodeConfigurationImpl( baseDirectory = baseDirectoryPath, - myLegalName = configuration[myLegalName], - emailAddress = configuration[emailAddress], - p2pAddress = configuration[p2pAddress], - keyStorePassword = configuration[keyStorePassword], - trustStorePassword = configuration[trustStorePassword], - crlCheckSoftFail = configuration[crlCheckSoftFail], - dataSourceProperties = configuration[dataSourceProperties], - rpcUsers = configuration[rpcUsers], - verifierType = configuration[verifierType], - flowTimeout = configuration[flowTimeout], - rpcSettings = configuration[rpcSettings], - messagingServerAddress = configuration[messagingServerAddress], - notary = configuration[notary], - flowOverrides = configuration[flowOverrides], - additionalP2PAddresses = configuration[additionalP2PAddresses], - additionalNodeInfoPollingFrequencyMsec = configuration[additionalNodeInfoPollingFrequencyMsec], - jmxMonitoringHttpPort = configuration[jmxMonitoringHttpPort], - security = configuration[security], - devMode = configuration[devMode], - devModeOptions = configuration[devModeOptions], - compatibilityZoneURL = configuration[compatibilityZoneURL], - networkServices = configuration[networkServices], - certificateChainCheckPolicies = configuration[certificateChainCheckPolicies], + myLegalName = config[myLegalName], + emailAddress = config[emailAddress], + p2pAddress = config[p2pAddress], + keyStorePassword = config[keyStorePassword], + trustStorePassword = config[trustStorePassword], + crlCheckSoftFail = config[crlCheckSoftFail], + dataSourceProperties = config[dataSourceProperties], + rpcUsers = config[rpcUsers], + verifierType = config[verifierType], + flowTimeout = config[flowTimeout], + rpcSettings = config[rpcSettings], + messagingServerAddress = config[messagingServerAddress], + notary = config[notary], + flowOverrides = config[flowOverrides], + additionalP2PAddresses = config[additionalP2PAddresses], + additionalNodeInfoPollingFrequencyMsec = config[additionalNodeInfoPollingFrequencyMsec], + jmxMonitoringHttpPort = config[jmxMonitoringHttpPort], + security = config[security], + devMode = config[devMode], + devModeOptions = config[devModeOptions], + compatibilityZoneURL = config[compatibilityZoneURL], + networkServices = config[networkServices], + certificateChainCheckPolicies = config[certificateChainCheckPolicies], messagingServerExternal = messagingServerExternal, - useTestClock = configuration[useTestClock], - lazyBridgeStart = configuration[lazyBridgeStart], - detectPublicIp = configuration[detectPublicIp], - sshd = configuration[sshd], - localShellAllowExitInSafeMode = configuration[localShellAllowExitInSafeMode], - localShellUnsafe = configuration[localShellUnsafe], + useTestClock = config[useTestClock], + lazyBridgeStart = config[lazyBridgeStart], + detectPublicIp = config[detectPublicIp], + sshd = config[sshd], + localShellAllowExitInSafeMode = config[localShellAllowExitInSafeMode], + localShellUnsafe = config[localShellUnsafe], database = database, - noLocalShell = configuration[noLocalShell], - attachmentCacheBound = configuration[attachmentCacheBound], - extraNetworkMapKeys = configuration[extraNetworkMapKeys], - tlsCertCrlDistPoint = configuration[tlsCertCrlDistPoint], - tlsCertCrlIssuer = configuration[tlsCertCrlIssuer], - h2Settings = configuration[h2Settings], - flowMonitorPeriodMillis = configuration[flowMonitorPeriodMillis], - flowMonitorSuspensionLoggingThresholdMillis = configuration[flowMonitorSuspensionLoggingThresholdMillis], - jmxReporterType = configuration[jmxReporterType], - rpcAddress = configuration[rpcAddress], - transactionCacheSizeMegaBytes = configuration[transactionCacheSizeMegaBytes], - attachmentContentCacheSizeMegaBytes = configuration[attachmentContentCacheSizeMegaBytes], - h2port = configuration[h2port], - jarDirs = configuration[jarDirs], + noLocalShell = config[noLocalShell], + attachmentCacheBound = config[attachmentCacheBound], + extraNetworkMapKeys = config[extraNetworkMapKeys], + tlsCertCrlDistPoint = config[tlsCertCrlDistPoint], + tlsCertCrlIssuer = config[tlsCertCrlIssuer], + h2Settings = config[h2Settings], + flowMonitorPeriodMillis = config[flowMonitorPeriodMillis], + flowMonitorSuspensionLoggingThresholdMillis = config[flowMonitorSuspensionLoggingThresholdMillis], + jmxReporterType = config[jmxReporterType], + rpcAddress = config[rpcAddress], + transactionCacheSizeMegaBytes = config[transactionCacheSizeMegaBytes], + attachmentContentCacheSizeMegaBytes = config[attachmentContentCacheSizeMegaBytes], + h2port = config[h2port], + jarDirs = config[jarDirs], cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) }, - cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist], - blacklistedAttachmentSigningKeys = configuration[blacklistedAttachmentSigningKeys], - networkParameterAcceptanceSettings = configuration[networkParameterAcceptanceSettings], - configurationWithOptions = ConfigurationWithOptions(configuration, Configuration.Validation.Options.defaults), - flowExternalOperationThreadPoolSize = configuration[flowExternalOperationThreadPoolSize], - quasarExcludePackages = configuration[quasarExcludePackages] + cordappSignerKeyFingerprintBlacklist = config[cordappSignerKeyFingerprintBlacklist], + blacklistedAttachmentSigningKeys = config[blacklistedAttachmentSigningKeys], + networkParameterAcceptanceSettings = config[networkParameterAcceptanceSettings], + configurationWithOptions = ConfigurationWithOptions(configuration, Configuration.Options.defaults), + flowExternalOperationThreadPoolSize = config[flowExternalOperationThreadPoolSize], + quasarExcludePackages = config[quasarExcludePackages] )) } catch (e: Exception) { return when (e) {