diff --git a/.ci/dev/compatibility/DockerfileJDK11 b/.ci/dev/compatibility/DockerfileJDK11 index ff6eeaac95..23aa144955 100644 --- a/.ci/dev/compatibility/DockerfileJDK11 +++ b/.ci/dev/compatibility/DockerfileJDK11 @@ -6,4 +6,4 @@ RUN apt-get update && apt-get install -y curl apt-transport-https \ software-properties-common \ wget ARG USER="stresstester" -RUN useradd -m ${USER} \ No newline at end of file +RUN useradd -m ${USER} diff --git a/.ci/dev/forward-merge/Jenkinsfile b/.ci/dev/forward-merge/Jenkinsfile index 169cdd258d..1faaa9d543 100644 --- a/.ci/dev/forward-merge/Jenkinsfile +++ b/.ci/dev/forward-merge/Jenkinsfile @@ -13,13 +13,13 @@ * the branch name of origin branch, it should match the current branch * and it acts as a fail-safe inside {@code forwardMerger} pipeline */ -String originBranch = 'release/os/4.8' +String originBranch = 'release/os/4.9' /** * the branch name of target branch, it should be the branch with the next version * after the one in current branch. */ -String targetBranch = 'release/os/4.9' +String targetBranch = 'release/os/4.10' /** * Forward merge any changes between #originBranch and #targetBranch diff --git a/.ci/dev/nightly-regression/Jenkinsfile b/.ci/dev/nightly-regression/Jenkinsfile index 06266dcb23..1c4f0bd520 100644 --- a/.ci/dev/nightly-regression/Jenkinsfile +++ b/.ci/dev/nightly-regression/Jenkinsfile @@ -92,6 +92,7 @@ pipeline { } stage('Recompile') { steps { + authenticateGradleWrapper() sh script: [ './gradlew', COMMON_GRADLE_PARAMS, diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index b565d1bb5e..d4126f51e5 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -6,9 +6,11 @@ @Library('corda-shared-build-pipeline-steps') import com.r3.build.utils.GitUtils +import com.r3.build.enums.SnykOrganisation +import com.r3.build.utils.SnykUtils GitUtils gitUtils = new GitUtils(this) - +SnykUtils snykUtils = new SnykUtils(this) /** * Sense environment */ @@ -16,7 +18,7 @@ boolean isReleaseBranch = (env.BRANCH_NAME =~ /^release\/os\/.*/) boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(?- + Guava’s Files.createTempDir() is used during integration tests only. + Users of Corda are advised not to use Guava’s Files.createTempDir() + when building applications on Corda. + expires: 2023-09-01T11:38:11.478Z + created: 2022-12-29T11:38:11.489Z + SNYK-JAVA-COMH2DATABASE-31685: + - '*': + reason: >- + H2 console is not enabled for any of the applications we are running. + + When it comes to DB connectivity parameters, we do not allow changing + them as they are supplied by Corda Node configuration file. + expires: 2023-09-01T11:39:26.763Z + created: 2022-12-29T11:39:26.775Z + SNYK-JAVA-COMH2DATABASE-2331071: + - '*': + reason: >- + H2 console is not enabled for any of the applications we are running. + + When it comes to DB connectivity parameters, we do not allow changing + them as they are supplied by Corda Node configuration file. + expires: 2023-09-01T11:41:05.707Z + created: 2022-12-29T11:41:05.723Z + SNYK-JAVA-COMSQUAREUPOKHTTP3-2958044: + - '*': + reason: >- + The vulnerability in okhttp’s error handling is only exploitable in + services that receive and parse HTTP requests. Corda does not receive + HTTP requests and thus is not exposed to this issue. + expires: 2023-09-01T11:42:55.546Z + created: 2022-12-29T11:42:55.556Z + SNYK-JAVA-IONETTY-1042268: + - '*': + reason: >- + Corda does not rely on hostname verification in the P2P protocol to + identify a host, so is not impacted by this vulnerability. Corda uses + its own SSL identity check logic for the network model. Corda + validates based on the full X500 subject name and the fact that P2P + links use mutually authenticated TLS with the same trust roots. For + RPC SSL client connections Artemis is used which calls into netty. The + default value for verifyHost is true for Artemis client connectors so + verification of the host name in netty does occur. + expires: 2023-09-01T11:45:42.976Z + created: 2022-12-29T11:45:42.981Z + SNYK-JAVA-ORGJETBRAINSKOTLIN-2628385: + - '*': + reason: >- + This is a build time vulnerability. It relates to the inability to + lock dependencies for Kotlin Multiplatform Gradle Projects. At build + time for Corda we do not use Multiplatform Gradle Projects so are not + affected by this vulnerability. In addition as it is a build time + vulnerability released artifacts are not affected. + expires: 2023-09-01T11:52:35.855Z + created: 2022-12-29T11:52:35.870Z + SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744: + - '*': + reason: >- + This vulnerability relates to information exposure via creation of + temporary files (via Kotlin functions) with insecure permissions. + Corda does not use any of the vulnerable functions so it not + susceptible to this vulnerability. + expires: 2023-09-01T13:39:03.244Z + created: 2022-12-29T13:39:03.262Z + SNYK-JAVA-ORGLIQUIBASE-2419059: + - '*': + reason: >- + This component is used to upgrade the node database schema either at + node startup or via the database migration tool. The XML input for the + database migration is generated by Corda from either R3 supplied XML + files included in corda.jar or those XML files written by the CorDapp + author included in a CorDapp that is installed in the node CorDapps + directory. Contract CorDapps received over the network are not a + source of XML files for this generation step. An attacker trying to + exploit this vulnerability would need access to the server with the + XML input files, and specifically the access and ability to change JAR + files on the file system that make up the Corda installation. + expires: 2023-09-01T13:42:11.552Z + created: 2022-12-29T13:42:11.570Z + SNYK-JAVA-COMH2DATABASE-2348247: + - '*': + reason: >- + H2 console is not enabled for any of the applications we are running. + When it comes to DB connectivity parameters, we do not allow changing + them as they are supplied by Corda Node configuration file. + expires: 2023-09-01T11:36:39.068Z + created: 2022-12-29T11:36:39.089Z + SNYK-JAVA-COMH2DATABASE-1769238: + - '*': + reason: >- + H2 is not invoked by Corda unless the node deployment configures an H2 + database. This is not a supported configuration in Production and so + this vulnerability should be irrelevant except during development on + Corda. Corda itself does not store XML data within the database so + Corda is not susceptible to this vulnerability. If CorDapp developers + store XML data to the database they need to ascertain themselves that + they are not susceptible. + expires: 2023-09-01T11:40:29.871Z + created: 2022-12-29T11:40:29.896Z + SNYK-JAVA-ORGYAML-3152153: + - '*': + reason: >- + There is a transitive dependency on snakeyaml from the third party + components jackson-dataformat-yaml and liquidbase-core. The + jackson-dataformat-yaml component does not use the snakeyaml + databinding layer. For liquidbase we use xml in the changelog files + not yaml. So given this Corda is not susceptible to this + vulnerability.Cordapp authors should exercise their own judgment if + using this library directly in their cordapp. + expires: 2023-09-01T11:35:04.385Z + created: 2023-01-04T11:35:04.414Z + SNYK-JAVA-COMH2DATABASE-3146851: + - '*': + reason: >- + Corda does not make use of the H2 web admin console, so it not + susceptible to this reported vulnerability + expires: 2023-09-01T11:45:11.295Z + created: 2023-01-04T11:45:11.322Z + SNYK-JAVA-ORGBOUNCYCASTLE-2841508: + - '*': + reason: >- + This vulnerability relates to weak key-hash message authentication + code due to an error within the BKS version 1 keystore files. Corda + does not use BKS-V1 for its keystore files so is not susceptible to + this vulnerability. + expires: 2023-09-01T11:32:38.120Z + created: 2022-09-21T11:32:38.125Z + SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038424: + - '*': + reason: >- + Corda does not set the non-default UNWRAP_SINGLE_VALUE_ARRAYS required + for this vulnerability. In addition Corda does not use Jackson for + deserialization except in the optional shell which we recommend using + standalone. The Corda node itself is not exposed. Corda does however + provide mappings of Corda types to allow CorDapps to use Jackson, and + CorDapps using Jackson should make their own assessment. This + vulnerability relates to deeply nested untyped Object or Array values + (3000 levels deep). Only CorDapps with these types at this level of + nesting are potentially susceptible. + expires: 2023-09-01T12:04:40.180Z + created: 2023-02-09T12:04:40.209Z + SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426: + - '*': + reason: >- + Corda does not set the non-default UNWRAP_SINGLE_VALUE_ARRAYS required + for this vulnerability. In addition Corda does not use Jackson for + deserialization except in the optional shell which we recommend using + standalone. The Corda node itself is not exposed. Corda does however + provide mappings of Corda types to allow CorDapps to use Jackson, and + CorDapps using Jackson should make their own assessment. This + vulnerability relates to deeply nested untyped Object or Array values + (3000 levels deep). Only CorDapps with these types at this level of + nesting are potentially susceptible. + expires: 2023-09-01T12:05:03.931Z + created: 2023-02-09T12:05:03.962Z + SNYK-JAVA-ORGYAML-2806360: + - '*': + reason: >- + Snakeyaml is being used by Jackson and liquidbase. Corda does not use + Jackson except in the optional shell which we recommend using + standalone. The Corda node itself is not exposed. Corda does however + provide mappings of Corda types to allow CorDapps to use Jackson, and + CorDapps using Jackson should make their own assessment. Liquibase is + used to apply the database migration changes. XML files are used here + to define the changes not YAML and therefore the Corda node itself is + not exposed to this DOS vulnerability. + expires: 2023-09-01T13:40:55.262Z + created: 2022-09-21T13:40:55.279Z + SNYK-JAVA-ORGYAML-3016891: + - '*': + reason: >- + Snakeyaml is being used by Jackson and liquidbase. Corda does not use + Jackson for deserialization except in the optional shell which we + recommend using standalone. The Corda node itself is not exposed. + Corda does however provide mappings of Corda types to allow CorDapps + to use Jackson, and CorDapps using Jackson should make their own + assessment. Liquibase is used to apply the database migration changes. + XML files are used here to define the changes not YAML and therefore + the Corda node itself is not exposed to this deserialisation + vulnerability. + expires: 2023-09-01T16:37:28.911Z + created: 2023-02-06T16:37:28.933Z + SNYK-JAVA-ORGYAML-3016888: + - '*': + reason: >- + Snakeyaml is being used by Jackson and liquidbase. Corda does not use + Jackson for deserialization except in the optional shell which we + recommend using standalone. The Corda node itself is not exposed. + Corda does however provide mappings of Corda types to allow CorDapps + to use Jackson, and CorDapps using Jackson should make their own + assessment. Liquibase is used to apply the database migration changes. + XML files are used here to define the changes not YAML and therefore + the Corda node itself is not exposed to this deserialisation + vulnerability. + expires: 2023-09-01T13:39:49.450Z + created: 2022-09-21T13:39:49.470Z + SNYK-JAVA-ORGYAML-3016889: + - '*': + reason: >- + Snakeyaml is being used by Jackson and liquidbase. Corda does not use + Jackson for deserialization except in the optional shell which we + recommend using standalone. The Corda node itself is not exposed. + Corda does however provide mappings of Corda types to allow CorDapps + to use Jackson, and CorDapps using Jackson should make their own + assessment. Liquibase is used to apply the database migration changes. + XML files are used here to define the changes not YAML and therefore + the Corda node itself is not exposed to this deserialisation + vulnerability. + expires: 2023-09-01T16:35:13.840Z + created: 2023-02-06T16:35:13.875Z + SNYK-JAVA-ORGYAML-3113851: + - '*': + reason: >- + Snakeyaml is being used by Jackson and liquidbase. Corda does not use + Jackson for deserialization except in the optional shell which we + recommend using standalone. The Corda node itself is not exposed. + Corda does however provide mappings of Corda types to allow CorDapps + to use Jackson, and CorDapps using Jackson should make their own + assessment. Liquibase is used to apply the database migration changes. + XML files are used here to define the changes not YAML and therefore + the Corda node itself is not exposed to this deserialisation + vulnerability. + expires: 2024-04-01T00:00:00.000Z + created: 2022-11-29T14:55:03.623Z +patch: {} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98a67e669c..da3fe3c032 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,4 @@ Corda is an open-source project and contributions are welcome! -To find out how to contribute, please see our [contributing docs](https://docs.corda.net/head/contributing-index.html). +To find out how to contribute, please see our [contributing docs](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html). diff --git a/README.md b/README.md index 78915358e4..a84c2d5957 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Corda is an open source blockchain project, designed for business from the start Corda is an open-source project and contributions are welcome! -To find out how to contribute, please see our [contributing docs](https://docs.corda.net/head/contributing-index.html). +To find out how to contribute, please see our [contributing docs](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html). ## License diff --git a/build.gradle b/build.gradle index a0a35a4039..a3ae5b8ab5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { ext.baseVersion = constants.getProperty("cordaVersion") ext.versionSuffix = constants.getProperty("versionSuffix") + ext.corda_build_edition = System.getenv("CORDA_BUILD_EDITION")?.trim() ?: "Corda Open Source" ext.corda_platform_version = constants.getProperty("platformVersion") ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") @@ -60,11 +61,11 @@ buildscript { ext.capsule_version = '1.0.3' ext.asm_version = '7.1' - ext.artemis_version = '2.6.2' - // TODO Upgrade to Jackson Kotlin 2.10+ only when corda is using kotlin 1.3.10 + ext.artemis_version = '2.19.1' + // TODO Upgrade Jackson only when corda is using kotlin 1.3.10 ext.jackson_version = '2.13.5' ext.jackson_kotlin_version = '2.9.7' - ext.jetty_version = '9.4.19.v20190610' + ext.jetty_version = '9.4.52.v20230823' ext.jersey_version = '2.25' ext.servlet_version = '4.0.1' ext.assertj_version = '3.12.2' @@ -80,7 +81,7 @@ buildscript { ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion') ext.okhttp_version = '3.14.2' ext.netty_version = '4.1.77.Final' - ext.tcnative_version = '2.0.48.Final' + ext.tcnative_version = constants.getProperty("tcnativeVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") ext.fileupload_version = '1.4' ext.kryo_version = '4.0.2' @@ -106,7 +107,6 @@ buildscript { ext.dependency_checker_version = '5.2.0' ext.commons_collections_version = '4.3' ext.beanutils_version = '1.9.4' - ext.crash_version = '1.7.6' ext.jsr305_version = constants.getProperty("jsr305Version") ext.shiro_version = '1.10.0' ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') @@ -130,6 +130,8 @@ buildscript { ext.controlsfx_version = '8.40.15' ext.detekt_version = constants.getProperty('detektVersion') ext.docker_java_version = constants.getProperty("dockerJavaVersion") + ext.commons_configuration2_version = "2.8.0" + ext.commons_text_version = "1.10.0" if (JavaVersion.current().isJava8()) { ext.fontawesomefx_commons_version = '8.15' ext.fontawesomefx_fontawesome_version = '4.7.0-5' @@ -362,7 +364,7 @@ allprojects { attributes('Corda-Release-Version': corda_release_version) attributes('Corda-Platform-Version': corda_platform_version) attributes('Corda-Revision': corda_revision) - attributes('Corda-Vendor': 'Corda Open Source') + attributes('Corda-Vendor': corda_build_edition) attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}") attributes('Corda-Docs-Link': corda_docs_link) } @@ -411,6 +413,12 @@ allprojects { repositories { mavenLocal() + // Prevents cache giving use the wrong artemis + mavenCentral { + content { + includeGroup 'org.apache.activemq' + } + } // Use system environment to activate caching with Artifactory, // because it is actually easier to pass that during parallel build. // NOTE: it has to be a name of a virtual repository with all @@ -453,6 +461,12 @@ allprojects { includeGroup 'com.github.detro' } } + maven { + url "${publicArtifactURL}/corda-releases" + content { + includeModule('net.corda', 'corda-shell') + } + } mavenCentral() jcenter() } @@ -469,7 +483,9 @@ allprojects { // Force dependencies to use the same version of Guava as Corda. force "com.google.guava:guava:$guava_version" - // Demand that everything uses our given version of Netty. + // Demand that everything uses our given versions of: + // * Netty + // * Apache commons-configuration2 eachDependency { details -> if (details.requested.group == 'io.netty' && details.requested.name.startsWith('netty-')) { if (details.requested.name.startsWith('netty-tcnative')){ @@ -479,6 +495,13 @@ allprojects { } } + if (details.requested.group == 'org.apache.commons') { + if (details.requested.name == "commons-configuration2") { + details.useVersion commons_configuration2_version + } else if (details.requested.name == "commons-text") { + details.useVersion commons_text_version + } + } if (details.requested.group == 'org.yaml' && details.requested.name == 'snakeyaml') { details.useVersion snake_yaml_version } diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index ae95844564..46d7c105f3 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -2,8 +2,21 @@ package net.corda.client.jackson import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.* -import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.databind.BeanDescription +import com.fasterxml.jackson.databind.DeserializationConfig +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.Module +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.cfg.ConstructorDetector @@ -22,9 +35,21 @@ import net.corda.core.DoNotImplement import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* -import net.corda.core.identity.* -import net.corda.core.internal.* +import net.corda.core.crypto.Base58 +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.CertRole +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.isStatic +import net.corda.core.internal.kotlinObjectInstance +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index af306035bd..24e3efd707 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -2,16 +2,30 @@ package net.corda.client.jackson.internal -import com.fasterxml.jackson.annotation.* import com.fasterxml.jackson.annotation.JsonAutoDetect.Value import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility +import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED +import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.databind.BeanDescription +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationConfig +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.Module +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationConfig +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.cfg.MapperConfig @@ -32,12 +46,30 @@ import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer import com.fasterxml.jackson.databind.ser.std.UUIDSerializer import com.google.common.primitives.Booleans import net.corda.client.jackson.JacksonSupport -import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.PartialMerkleTree.PartialTree +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash.Companion.SHA2_256 +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.StateMachineRunId -import net.corda.core.identity.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.createComponentGroups import net.corda.core.node.NodeInfo @@ -45,7 +77,12 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.* +import net.corda.core.transactions.ContractUpgradeFilteredTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.FilteredTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.parseAsHex diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index d8a1b514ec..06c1bbbd93 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -55,7 +55,9 @@ dependencies { // TODO: remove the forced update of commons-collections and beanutils when artemis updates them compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}" - compile "org.apache.activemq:artemis-core-client:${artemis_version}" + compile("org.apache.activemq:artemis-core-client:${artemis_version}") { + exclude group: 'org.jgroups', module: 'jgroups' + } // Unit testing helpers. testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index e476e0e581..94effe653a 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -30,6 +30,7 @@ import net.corda.testing.node.internal.rpcTestUser import net.corda.testing.node.internal.startRandomRpcClient import net.corda.testing.node.internal.startRpcClient import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.junit.After import org.junit.Assert.assertEquals @@ -551,7 +552,11 @@ class RPCStabilityTests { // Construct an RPC session manually so that we can hang in the message handler val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val session = startArtemisSession(server.broker.hostAndPort!!) - session.createTemporaryQueue(myQueue, ActiveMQDefaultConfiguration.getDefaultRoutingType(), myQueue) + session.createQueue(QueueConfiguration(myQueue) + .setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType()) + .setAddress(myQueue) + .setTemporary(true) + .setDurable(false)) val consumer = session.createConsumer(myQueue, null, -1, -1, false) consumer.setMessageHandler { Thread.sleep(5000) // Needs to be slower than one per second to get kicked. @@ -588,7 +593,11 @@ class RPCStabilityTests { // Construct an RPC client session manually val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val session = startArtemisSession(server.broker.hostAndPort!!) - session.createTemporaryQueue(myQueue, ActiveMQDefaultConfiguration.getDefaultRoutingType(), myQueue) + session.createQueue(QueueConfiguration(myQueue) + .setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType()) + .setAddress(myQueue) + .setTemporary(true) + .setDurable(false)) val consumer = session.createConsumer(myQueue, null, -1, -1, false) val replies = ArrayList() consumer.setMessageHandler { diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt index b7bdc19f50..3ae1838664 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpcreconnect/CordaRPCClientReconnectionTest.kt @@ -50,6 +50,7 @@ import kotlin.concurrent.thread import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNull import kotlin.test.assertTrue class CordaRPCClientReconnectionTest { @@ -595,6 +596,29 @@ class CordaRPCClientReconnectionTest { } } + @Test(timeout=300_000) + fun `reconnecting 'reattachFlowWithClientId' rpc works if called with non-existent client id`() { + driver(DriverParameters(inMemoryDB = false, cordappsForAllNodes = listOf(this.enclosedCordapp()))) { + val address = NetworkHostAndPort("localhost", portAllocator.nextPort()) + fun startNode(additionalCustomOverrides: Map = emptyMap()): NodeHandle { + return startNode( + providedName = CHARLIE_NAME, + rpcUsers = listOf(CordaRPCClientTest.rpcUser), + customOverrides = mapOf("rpcSettings.address" to address.toString()) + additionalCustomOverrides + ).getOrThrow() + } + + val node = startNode() + val client = CordaRPCClient(node.rpcAddress, config) + (client.start(rpcUser.username, rpcUser.password, gracefulReconnect = gracefulReconnect)).use { + val rpcOps = it.proxy as ReconnectingCordaRPCOps + val nonExistentClientId = UUID.randomUUID().toString() + val flowHandle = rpcOps.reattachFlowWithClientId(nonExistentClientId) + assertNull(flowHandle) + } + } + } + @StartableByRPC class SimpleFlow : FlowLogic() { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index ea5a54cef2..c44ce973fe 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -98,6 +98,8 @@ class RPCClient( // By default RoundRobinConnectionLoadBalancingPolicy is used that picks first endpoint from the pool // at random. This may be undesired and non-deterministic. For more information, see [RoundRobinConnectionPolicy] connectionLoadBalancingPolicyClassName = RoundRobinConnectionPolicy::class.java.canonicalName + // Without this any type of "send" time failures will not be delivered back to the client + isBlockOnNonDurableSend = true } val sessionId = Trace.SessionId.newInstance() val distributionMux = DistributionMux(listeners, username) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 82ac3cd579..c13266264b 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -39,6 +39,7 @@ import net.corda.nodeapi.internal.rpc.client.RpcClientObservableDeSerializer import net.corda.nodeapi.internal.rpc.client.RpcObservableMap import org.apache.activemq.artemis.api.core.ActiveMQException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -60,6 +61,7 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.Future import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit @@ -380,11 +382,18 @@ internal class RPCClientProxyHandler( targetLegalIdentity?.let { artemisMessage.putStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY, it.toString()) } - sendExecutor!!.submit { + val future: Future<*> = sendExecutor!!.submit { artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) log.debug { "-> RPC -> $message" } - rpcProducer!!.send(artemisMessage) + rpcProducer!!.let { + if (!it.isClosed) { + it.send(artemisMessage) + } else { + log.info("Producer is already closed. Not sending: $message") + } + } } + future.getOrThrow() } // The handler for Artemis messages. @@ -570,7 +579,12 @@ internal class RPCClientProxyHandler( } if (observableIds != null) { log.debug { "Reaping ${observableIds.size} observables" } - sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds)) + @Suppress("TooGenericExceptionCaught") + try { + sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds)) + } catch(ex: Exception) { + log.warn("Unable to close observables", ex) + } } } @@ -632,7 +646,8 @@ internal class RPCClientProxyHandler( consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, 16384) clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$rpcUsername.${random63BitValue()}") log.debug { "Client address: $clientAddress" } - consumerSession!!.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) + consumerSession!!.createQueue(QueueConfiguration(clientAddress).setAddress(clientAddress).setRoutingType(RoutingType.ANYCAST) + .setTemporary(true).setDurable(false)) rpcConsumer = consumerSession!!.createConsumer(clientAddress) rpcConsumer!!.setMessageHandler(this::artemisMessageHandler) } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt index 005ac70fd3..afaa51fe0b 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt @@ -393,10 +393,11 @@ class ReconnectingCordaRPCOps private constructor( initialFeed.copy(updates = observable) } FlowHandleWithClientId::class.java -> { - val initialHandle: FlowHandleWithClientId = uncheckedCast(doInvoke(method, args, + // initialHandle can be null. See @CordaRPCOps.reattachFlowWithClientId. + val initialHandle: FlowHandleWithClientId? = uncheckedCast(doInvoke(method, args, reconnectingRPCConnection.gracefulReconnect.maxAttempts)) - val initialFuture = initialHandle.returnValue + val initialFuture = initialHandle?.returnValue ?: return null // This is the future that is returned to the client. It will get carried until we reconnect to the node. val returnFuture = openFuture() diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index dea9797cd2..49d3680e54 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -40,7 +40,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.sleeping.SleepingFlow import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess -import org.apache.commons.io.output.NullOutputStream +import org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM import org.hamcrest.text.MatchesPattern import org.junit.After import org.junit.Before @@ -117,7 +117,7 @@ class StandaloneCordaRPClientTest { assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash") val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it -> - it.copyTo(NullOutputStream()) + it.copyTo(NULL_OUTPUT_STREAM) SecureHash.SHA256(it.hash().asBytes()) } assertEquals(attachment.sha256, hash) @@ -132,7 +132,7 @@ class StandaloneCordaRPClientTest { assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash") val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it -> - it.copyTo(NullOutputStream()) + it.copyTo(NULL_OUTPUT_STREAM) SecureHash.SHA256(it.hash().asBytes()) } assertEquals(attachment.sha256, hash) diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt index aa5d86d5d7..96c9ac0c07 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt @@ -9,4 +9,4 @@ package net.corda.common.logging * (originally added to source control for ease of use) */ -internal const val CURRENT_MAJOR_RELEASE = "4.8-SNAPSHOT" \ No newline at end of file +internal const val CURRENT_MAJOR_RELEASE = "4.9-SNAPSHOT" \ No newline at end of file diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index 02aa604cf4..03e283ef90 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -135,7 +135,7 @@ - + @@ -159,7 +159,7 @@ - + @@ -202,7 +202,15 @@ - + + + + + + + + + diff --git a/constants.properties b/constants.properties index 119bf179b3..a30f2432ac 100644 --- a/constants.properties +++ b/constants.properties @@ -2,7 +2,7 @@ # because some versions here need to be matched by app authors in # their own projects. So don't get fancy with syntax! -cordaVersion=4.8 +cordaVersion=4.9 versionSuffix=SNAPSHOT gradlePluginsVersion=5.0.12 kotlinVersion=1.2.71 @@ -11,7 +11,7 @@ java8MinUpdateVersion=171 # When incrementing platformVersion make sure to update # # net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. # # ***************************************************************# -platformVersion=10 +platformVersion=11 guavaVersion=28.0-jre # Quasar version to use with Java 8: quasarVersion=0.7.15_r3 @@ -27,7 +27,7 @@ typesafeConfigVersion=1.3.4 jsr305Version=3.0.2 artifactoryPluginVersion=4.16.1 snakeYamlVersion=1.33 -caffeineVersion=2.7.0 +caffeineVersion=2.9.3 metricsVersion=4.1.0 metricsNewRelicVersion=1.1.1 djvmVersion=1.1.1 @@ -36,3 +36,4 @@ openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 detektVersion=1.0.1 +tcnativeVersion=2.0.48.Final diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index e0f41d2fdd..3e09be0ff4 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -28,7 +28,9 @@ import java.util.jar.JarInputStream // *Internal* Corda-specific utilities. -const val PLATFORM_VERSION = 10 + +// When incrementing platformVersion make sure to update PLATFORM_VERSION in constants.properties as well. +const val PLATFORM_VERSION = 11 fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature) diff --git a/core/src/main/kotlin/net/corda/core/internal/messaging/FlowManagerRPCOps.kt b/core/src/main/kotlin/net/corda/core/internal/messaging/FlowManagerRPCOps.kt index 373fa36f09..845c600d7d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/messaging/FlowManagerRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/internal/messaging/FlowManagerRPCOps.kt @@ -5,13 +5,20 @@ import net.corda.core.messaging.RPCOps /** * RPC operations to perform operations related to flows including management of associated persistent states like checkpoints. */ +@Deprecated( + "A public version of this interface has been exposed that should be interacted with using the MultiRPCClient", + ReplaceWith("net.corda.core.messaging.flows.FlowManagerRPCOps") +) interface FlowManagerRPCOps : RPCOps { + /** * Dump all the current flow checkpoints as JSON into a zip file in the node's log directory. */ fun dumpCheckpoints() - /** Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars - * into a zip file in the node's log directory. */ + /** + * Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars into a zip file in the node's + * log directory. + */ fun debugCheckpoints() } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/messaging/flows/FlowManagerRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/flows/FlowManagerRPCOps.kt new file mode 100644 index 0000000000..4c7e3a8da8 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/messaging/flows/FlowManagerRPCOps.kt @@ -0,0 +1,20 @@ +package net.corda.core.messaging.flows + +import net.corda.core.messaging.RPCOps + +/** + * RPC operations to perform operations related to flows including management of associated persistent states like checkpoints. + */ +interface FlowManagerRPCOps : RPCOps { + + /** + * Dump all the current flow checkpoints as JSON into a zip file in the node's log directory. + */ + fun dumpCheckpoints() + + /** + * Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars into a zip file in the node's + * log directory. + */ + fun debugCheckpoints() +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 71fb6c728b..c05f378223 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -741,9 +741,11 @@ open class TransactionBuilder( addReferenceState(resolvedStateAndRef.referenced()) } } else { - log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + - "state pointers outside of flows. If you are writing a unit test then pass in a " + - "MockServices instance.") + if (nextStatePointer.isResolved) { + log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + + "state pointers outside of flows. If you are writing a unit test then pass in a " + + "MockServices instance.") + } return } } diff --git a/detekt-baseline.xml b/detekt-baseline.xml index d6fdc13c8c..046c21e008 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1364,7 +1364,6 @@ ThrowsCount:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String) ThrowsCount:AbstractNode.kt$AbstractNode$private fun installCordaServices() ThrowsCount:ArtemisMessagingServer.kt$ArtemisMessagingServer$// TODO: Maybe wrap [IOException] on a key store load error so that it's clearly splitting key store loading from // Artemis IO errors @Throws(IOException::class, AddressBindingException::class, KeyStoreException::class) private fun configureAndStartServer() - ThrowsCount:BrokerJaasLoginModule.kt$BaseBrokerJaasLoginModule$@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<javax.security.cert.X509Certificate>?> ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$ fun verifyCheckpointsCompatible( checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any> ) ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$// Throws exception when the flow is incompatible private fun checkFlowCompatible(subFlow: SubFlow, currentCordappsByHash: Map<SecureHash.SHA256, Cordapp>, platformVersion: Int) ThrowsCount:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema) diff --git a/docker/build.gradle b/docker/build.gradle index 394826e3a7..09fb2ba8e6 100644 --- a/docker/build.gradle +++ b/docker/build.gradle @@ -33,47 +33,34 @@ shadowJar { } enum ImageVariant { - UBUNTU_ZULU("zulu", "Dockerfile", "1.8"), - UBUNTU_ZULU_11("zulu", "Dockerfile11", "11"), - AL_CORRETTO("corretto", "DockerfileAL", "1.8"), + UBUNTU_ZULU("Dockerfile", "1.8", "zulu-openjdk8"), + UBUNTU_ZULU_11("Dockerfile11", "11", "zulu-openjdk11"), + AL_CORRETTO("DockerfileAL", "1.8", "amazonlinux2"), OFFICIAL(UBUNTU_ZULU) - String knownAs String dockerFile String javaVersion - - String versionString(String baseTag, String version) { - return "${baseTag}-${knownAs}" + - (knownAs.isEmpty() ? "" : "-") + - "java${javaVersion}-" + version - } + String baseImgaeFullName ImageVariant(ImageVariant other) { - this.knownAs = other.knownAs this.dockerFile = other.dockerFile this.javaVersion = other.javaVersion + this.baseImgaeFullName = other.baseImgaeFullName } - ImageVariant(String knownAs, String dockerFile, String javaVersion) { - this.knownAs = knownAs + ImageVariant(String dockerFile, String javaVersion, String baseImgaeFullName) { this.dockerFile = dockerFile this.javaVersion = javaVersion + this.baseImgaeFullName = baseImgaeFullName } static final String getRepository(Project project) { return project.properties.getOrDefault("docker.image.repository", "corda/corda") } - static private final String runTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) - - def getName(Project project) { - return versionString(getRepository(project), project.version.toString().toLowerCase()) - } - Set buildTags(Project project) { - final String suffix = project.version.toString().toLowerCase().contains("snapshot") ? runTime : "RELEASE" - return [suffix, "latest"].stream().map { - toAppend -> "${getName(project)}:${toAppend}".toString() + return ["${project.version.toString().toLowerCase()}-${baseImgaeFullName}"].stream().map { + toAppend -> "${getRepository(project)}:${toAppend}".toString() }.map(Identifier.&fromCompoundString).collect(Collectors.toSet()) } diff --git a/docker/src/bash/example-mini-network.sh b/docker/src/bash/example-mini-network.sh index 9b08f0ee61..4f25988ccd 100755 --- a/docker/src/bash/example-mini-network.sh +++ b/docker/src/bash/example-mini-network.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash NODE_LIST=("dockerNode1" "dockerNode2" "dockerNode3") NETWORK_NAME=mininet -CORDAPP_VERSION="4.8-SNAPSHOT" -DOCKER_IMAGE_VERSION="corda-zulu-4.8-snapshot" +CORDAPP_VERSION="4.9-SNAPSHOT" +DOCKER_IMAGE_VERSION="corda-zulu-4.9-snapshot" mkdir cordapps rm -f cordapps/* diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile index 48a0e330ee..84b259361b 100644 --- a/docker/src/docker/Dockerfile +++ b/docker/src/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:8u312 +FROM azul/zulu-openjdk:8u382 ## Remove Azul Zulu repo, as it is gone by now RUN rm -rf /etc/apt/sources.list.d/zulu.list diff --git a/docker/src/docker/Dockerfile-debug b/docker/src/docker/Dockerfile-debug index a1175c989c..aa19ffcd5b 100644 --- a/docker/src/docker/Dockerfile-debug +++ b/docker/src/docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:8u312 +FROM azul/zulu-openjdk:8u382 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN apt-get update && \ diff --git a/docker/src/docker/DockerfileAL b/docker/src/docker/DockerfileAL index f3c8496604..7447d84496 100644 --- a/docker/src/docker/DockerfileAL +++ b/docker/src/docker/DockerfileAL @@ -1,11 +1,10 @@ -FROM amazonlinux:2 +FROM amazoncorretto:8u382-al2 ## Add packages, clean cache, create dirs, create corda user and change ownership -RUN amazon-linux-extras enable corretto8 && \ - yum -y install java-1.8.0-amazon-corretto-devel && \ - yum -y install bash && \ +RUN yum -y install bash && \ yum -y install curl && \ yum -y install unzip && \ + yum -y install shadow-utils.x86_64 && \ yum clean all && \ rm -rf /var/cache/yum && \ mkdir -p /opt/corda/cordapps && \ diff --git a/node-api/build.gradle b/node-api/build.gradle index bb2280e23b..c4cdfdd906 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -17,7 +17,9 @@ dependencies { // TODO: remove the forced update of commons-collections and beanutils when artemis updates them compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}" - compile "org.apache.activemq:artemis-core-client:${artemis_version}" + compile("org.apache.activemq:artemis-core-client:${artemis_version}") { + exclude group: 'org.jgroups', module: 'jgroups' + } compile "org.apache.activemq:artemis-commons:${artemis_version}" compile "io.netty:netty-handler-proxy:$netty_version" @@ -65,6 +67,7 @@ dependencies { compile ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { // Gains our proton-j version from core module. exclude group: 'org.apache.qpid', module: 'proton-j' + exclude group: 'org.jgroups', module: 'jgroups' } } 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 74d580a827..1c914c35c4 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 @@ -67,7 +67,6 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration, retryInterval = messagingServerConnectionConfig.retryInterval().toMillis() retryIntervalMultiplier = messagingServerConnectionConfig.retryIntervalMultiplier() maxRetryInterval = messagingServerConnectionConfig.maxRetryInterval(isHA).toMillis() - isFailoverOnInitialConnection = messagingServerConnectionConfig.failoverOnInitialAttempt(isHA) initialConnectAttempts = messagingServerConnectionConfig.initialConnectAttempts(isHA) } addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize)) 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 84d63df5e2..6b5b353b3e 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 @@ -53,7 +53,7 @@ class ArtemisTcpTransport { keyStore?.let { with (it) { path.requireOnDefaultFileSystem() - options[TransportConstants.KEYSTORE_PROVIDER_PROP_NAME] = "JKS" + options[TransportConstants.KEYSTORE_TYPE_PROP_NAME] = "JKS" options[TransportConstants.KEYSTORE_PATH_PROP_NAME] = path options[TransportConstants.KEYSTORE_PASSWORD_PROP_NAME] = get().password } @@ -61,7 +61,7 @@ class ArtemisTcpTransport { trustStore?.let { with (it) { path.requireOnDefaultFileSystem() - options[TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME] = "JKS" + options[TransportConstants.TRUSTSTORE_TYPE_PROP_NAME] = "JKS" options[TransportConstants.TRUSTSTORE_PATH_PROP_NAME] = path options[TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME] = get().password } @@ -72,13 +72,13 @@ class ArtemisTcpTransport { private fun ClientRpcSslOptions.toTransportOptions() = mapOf( TransportConstants.SSL_ENABLED_PROP_NAME to true, - TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider, + TransportConstants.TRUSTSTORE_TYPE_PROP_NAME to trustStoreProvider, TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath, TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword) private fun BrokerRpcSslOptions.toTransportOptions() = mapOf( TransportConstants.SSL_ENABLED_PROP_NAME to true, - TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", + TransportConstants.KEYSTORE_TYPE_PROP_NAME to "JKS", TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath, TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false) @@ -186,10 +186,7 @@ class ArtemisTcpTransport { options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 if (trustManagerFactory != null) { // NettyAcceptor only creates default TrustManagerFactorys with the provided trust store details. However, we need to use - // more customised instances which use our revocation checkers, which we pass directly into NodeNettyAcceptorFactory. - // - // This, however, requires copying a lot of code from NettyAcceptor into NodeNettyAcceptor. The version of Artemis in - // Corda 4.9 solves this problem by introducing a "trustManagerFactoryPlugin" config option. + // more customised instances which use our revocation checkers, so we pass them in, to be picked up by Node(Open)SSLContextFactory. options[TRUST_MANAGER_FACTORY_NAME] = trustManagerFactory } return createTransport( @@ -211,6 +208,10 @@ class ArtemisTcpTransport { threadPoolName: String, trace: Boolean, remotingThreads: Int?): TransportConfiguration { + if (enableSSL) { + // This is required to stop Client checking URL address vs. Server provided certificate + options[TransportConstants.VERIFY_HOST_PROP_NAME] = false + } return createTransport( CordaNettyConnectorFactory::class.java.name, hostAndPort, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt index 5b813bff0a..18fed0658a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt @@ -10,11 +10,11 @@ import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor(maxMessageSize), Interceptor { - override fun getMessageSize(packet: Packet?): Int? { + override fun getMessageSize(packet: Packet?): Long? { return when (packet) { // This is an estimate of how much memory a Message body takes up. // Note, it is only an estimate - is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4).toInt() + is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4) // Skip all artemis control messages. else -> null } @@ -22,7 +22,7 @@ class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChec } class AmqpMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor(maxMessageSize), AmqpInterceptor { - override fun getMessageSize(packet: AMQPMessage?): Int? = packet?.encodeSize + override fun getMessageSize(packet: AMQPMessage?): Long? = packet?.wholeMessageSize } /** @@ -45,6 +45,6 @@ sealed class MessageSizeChecksInterceptor(private val maxMessageSize: I } // get size of the message in byte, returns null if the message is null or size don't need to be checked. - abstract fun getMessageSize(packet: T?): Int? + abstract fun getMessageSize(packet: T?): Long? } 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 708588cb63..357088bc0a 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 @@ -17,6 +17,7 @@ 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.QueueConfiguration 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 @@ -105,7 +106,9 @@ class BridgeControlListener(private val keyStore: CertificateStore, private fun registerBridgeControlListener(artemisSession: ClientSession) { try { - artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue) + artemisSession.createQueue( + QueueConfiguration(bridgeControlQueue).setAddress(BRIDGE_CONTROL).setRoutingType(RoutingType.MULTICAST) + .setTemporary(true).setDurable(false)) } catch (ex: ActiveMQQueueExistsException) { // Ignore if there is a queue still not cleaned up } @@ -125,7 +128,9 @@ class BridgeControlListener(private val keyStore: CertificateStore, private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) { try { - artemisSession.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue) + artemisSession.createQueue( + QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) + .setTemporary(true).setDurable(false)) } catch (ex: ActiveMQQueueExistsException) { // Ignore if there is a queue still not cleaned up } 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 index 2b1bf7829a..1f58a79219 100644 --- 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 @@ -15,7 +15,6 @@ import java.time.Duration * * 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 @@ -27,7 +26,6 @@ import java.time.Duration 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 @@ -36,7 +34,6 @@ enum class MessagingServerConnectionConfiguration { }, 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 @@ -46,7 +43,6 @@ enum class MessagingServerConnectionConfiguration { }, 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 @@ -54,7 +50,6 @@ enum class MessagingServerConnectionConfiguration { 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 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index 9b8ed573d2..0d69706eb1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -112,12 +112,11 @@ class DatabaseTransaction( } finally { clearException() contextTransactionOrNull = outerTransaction - } - - if (outerTransaction == null) { - synchronized(this) { - closed = true - boundary.onNext(CordaPersistence.Boundary(id, committed)) + if (outerTransaction == null) { + synchronized(this) { + closed = true + boundary.onNext(CordaPersistence.Boundary(id, committed)) + } } } } 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 38062c20b9..4ec23b9ba8 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 @@ -47,6 +47,7 @@ internal class ConnectionStateMachine(private val serverMode: Boolean, companion object { 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 const val CREATE_ADDRESS_PERMISSION_ERROR = "AMQ229032" 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) @@ -350,14 +351,35 @@ internal class ConnectionStateMachine(private val serverMode: Boolean, override fun onLinkRemoteClose(e: Event) { val link = e.link - if(link.remoteCondition != null) { - logWarnWithMDC("Connection closed due to error on remote side: `${link.remoteCondition.description}`") + if (link.remoteCondition != null) { + val remoteConditionDescription = link.remoteCondition.description + logWarnWithMDC("Connection closed due to error on remote side: `$remoteConditionDescription`") + // Description normally looks as follows: + // "AMQ229032: User: SystemUsers/Peer does not have permission='CREATE_ADDRESS' on address p2p.inbound.Test" + if (remoteConditionDescription.contains(CREATE_ADDRESS_PERMISSION_ERROR)) { + handleRemoteCreatePermissionError(e) + } + transport.condition = link.condition transport.close_tail() transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code) } } + /** + * If an the artemis channel does not exist on the counterparty, then a create permission error is returned in the [event]. + * Do not retry messages to this channel as it will result in an infinite loop of retries. + * Log the error, mark the messages as acknowledged and clear them from the message queue. + */ + private fun handleRemoteCreatePermissionError(event: Event) { + val remoteP2PAddress = event.sender.source.address + logWarnWithMDC("Address does not exist on peer: $remoteP2PAddress. Marking messages sent to this address as Acknowledged.") + messageQueues[remoteP2PAddress]?.apply { + forEach { it.doComplete(MessageStatus.Acknowledged) } + clear() + } + } + override fun onLinkFinal(event: Event) { val link = event.link if (link is Sender) { 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 7bb8e9ad39..62548b06c5 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 @@ -300,6 +300,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, cause is SSLException && (cause.message?.contains("close_notify") == true) -> logWarnWithMDC("Received close_notify during handshake") // io.netty.handler.ssl.SslHandler.setHandshakeFailureTransportFailure() cause is SSLException && (cause.message?.contains("writing TLS control frames") == true) -> logWarnWithMDC(cause.message!!) + cause is SSLException && (cause.message?.contains("internal_error") == true) -> logWarnWithMDC("Received internal_error during handshake") else -> badCert = true } if (log.isTraceEnabled) { diff --git a/node/build.gradle b/node/build.gradle index fe895177f7..3e247a5ed5 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -94,7 +94,7 @@ processTestResources { dependencies { compile project(':node-api') compile project(':client:rpc') - compile project(':tools:shell') + compile project(':client:jackson') compile project(':tools:cliutils') compile project(':common-validation') compile project(':common-configuration-parsing') @@ -127,11 +127,17 @@ dependencies { // TODO: remove the forced update of commons-collections and beanutils when artemis updates them compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}" - compile "org.apache.activemq:artemis-server:${artemis_version}" - compile "org.apache.activemq:artemis-core-client:${artemis_version}" + compile("org.apache.activemq:artemis-server:${artemis_version}") { + exclude group: 'org.apache.commons', module: 'commons-dbcp2' + exclude group: 'org.jgroups', module: 'jgroups' + } + compile("org.apache.activemq:artemis-core-client:${artemis_version}") { + exclude group: 'org.jgroups', module: 'jgroups' + } runtime("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { // Gains our proton-j version from core module. exclude group: 'org.apache.qpid', module: 'proton-j' + exclude group: 'org.jgroups', module: 'jgroups' } // Manifests: for reading stuff from the manifest file @@ -200,7 +206,6 @@ dependencies { // BFT-Smart dependencies compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' - compile 'commons-codec:commons-codec:1.13' // Java Atomix: RAFT library compile 'io.atomix.copycat:copycat-client:1.2.3' diff --git a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt index b1afd23f0a..5687bfa9d3 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt @@ -3,6 +3,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.PermissionException +import net.corda.client.rpc.RPCException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC @@ -151,7 +152,7 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) { proxy.stateMachinesFeed() assertFailsWith( PermissionException::class, - "This user should not be authorized to call 'nodeInfo'") { + "This user should not be authorized to call 'stateMachinesFeed'") { proxy.nodeInfo() } } @@ -185,7 +186,7 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) { val proxy = it.proxy assertFailsWith( PermissionException::class, - "This user should not be authorized to call 'nodeInfo'") { + "This user should not be authorized to call 'stateMachinesFeed'") { proxy.stateMachinesFeed() } db.addRoleToUser("user3", "default") @@ -207,8 +208,8 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) { db.deleteUser("user4") Thread.sleep(1500) assertFailsWith( - PermissionException::class, - "This user should not be authorized to call 'nodeInfo'") { + RPCException::class, + "This user should not be authorized to call 'stateMachinesFeed'") { proxy.stateMachinesFeed() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt index 2fb9e35196..646772745f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt @@ -13,6 +13,7 @@ import kotlin.test.assertTrue class NodeRPCTests { private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(\\.\\d+)?(-\\w+)?".toRegex() private val CORDA_VENDOR = "Corda Open Source" + private val CORDA_VENDOR_CE = "Corda Community Edition" private val CORDAPPS = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP) private val CORDAPP_TYPES = setOf("Contract CorDapp", "Workflow CorDapp") private val CLASSIFIER = if (SystemUtils.IS_JAVA_11) "-jdk11" else "" @@ -29,7 +30,7 @@ class NodeRPCTests { val nodeDiagnosticInfo = startNode().get().rpc.nodeDiagnosticInfo() assertTrue(nodeDiagnosticInfo.version.matches(CORDA_VERSION_REGEX)) assertEquals(PLATFORM_VERSION, nodeDiagnosticInfo.platformVersion) - assertEquals(CORDA_VENDOR, nodeDiagnosticInfo.vendor) + assertTrue(nodeDiagnosticInfo.vendor == CORDA_VENDOR || nodeDiagnosticInfo.vendor == CORDA_VENDOR_CE) nodeDiagnosticInfo.cordapps.forEach { println("${it.shortName} ${it.type}") } assertEquals(CORDAPPS.size, nodeDiagnosticInfo.cordapps.size) assertEquals(CORDAPP_TYPES, nodeDiagnosticInfo.cordapps.map { it.type }.toSet()) 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 a6d03bc337..c099cf3e7c 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 @@ -24,6 +24,7 @@ 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.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.junit.Assert.assertArrayEquals @@ -222,7 +223,8 @@ class AMQPBridgeTest { val artemis = artemisClient.started!! if (sourceQueueName != null) { // Local queue for outgoing messages - artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) + artemis.session.createQueue( + QueueConfiguration(sourceQueueName).setRoutingType(RoutingType.ANYCAST).setAddress(sourceQueueName).setDurable(true)) bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) } return Triple(artemisServer, artemisClient, bridgeManager) 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 ddffb79506..d7649fcfef 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 @@ -37,6 +37,7 @@ import net.corda.testing.node.internal.network.CrlServer import net.corda.testing.node.internal.network.CrlServer.Companion.EMPTY_CRL import net.corda.testing.node.internal.network.CrlServer.Companion.NODE_CRL import net.corda.testing.node.internal.network.CrlServer.Companion.withCrlDistPoint +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -496,7 +497,9 @@ class ArtemisServerRevocationTest : AbstractServerRevocationTest() { } val queueName = "${P2P_PREFIX}Test" - artemisNode.client.started!!.session.createQueue(queueName, RoutingType.ANYCAST, queueName, true) + artemisNode.client.started!!.session.createQueue( + QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName).setDurable(true) + ) val clientConnectionChangeStatus = client.waitForInitialConnectionAndCaptureChanges(expectedConnectedStatus) 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 f6e4e1d4ed..25f6dce6e8 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 @@ -36,6 +36,7 @@ import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.testing.internal.createDevIntermediateCaCertPath +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertArrayEquals @@ -272,7 +273,8 @@ class ProtonWrapperTests { assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) val artemis = artemisClient.started!! val sendAddress = P2P_PREFIX + "Test" - artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) + artemis.session.createQueue(QueueConfiguration("queue") + .setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true)) val consumer = artemis.session.createConsumer("queue") val testData = "Test".toByteArray() val testProperty = mutableMapOf() @@ -290,23 +292,26 @@ class ProtonWrapperTests { @Test(timeout=300_000) fun `Send a message larger then maxMessageSize from AMQP to Artemis inbox`() { - val maxMessageSize = 100_000 - val (server, artemisClient) = createArtemisServerAndClient(maxMessageSize) - val amqpClient = createClient(maxMessageSize) + val maxUserPayloadSize = 100_000 + val maxMessageSizeWithHeaders = maxUserPayloadSize + 512 // Adding a small "shim" to account for headers + // and other non-payload bits of data + val (server, artemisClient) = createArtemisServerAndClient(maxMessageSizeWithHeaders) + val amqpClient = createClient(maxMessageSizeWithHeaders) val clientConnected = amqpClient.onConnection.toFuture() amqpClient.start() assertEquals(true, clientConnected.get().connected) assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) val artemis = artemisClient.started!! val sendAddress = P2P_PREFIX + "Test" - artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) + artemis.session.createQueue(QueueConfiguration("queue") + .setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true)) val consumer = artemis.session.createConsumer("queue") val testProperty = mutableMapOf() testProperty["TestProp"] = "1" // Send normal message. - val testData = ByteArray(maxMessageSize) + val testData = ByteArray(maxUserPayloadSize) val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) amqpClient.write(message) assertEquals(MessageStatus.Acknowledged, message.onComplete.get()) @@ -314,8 +319,8 @@ class ProtonWrapperTests { assertEquals("1", received.getStringProperty("TestProp")) assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) }) - // Send message larger then max message size. - val largeData = ByteArray(maxMessageSize + 1) + // Send message larger than max message size. + val largeData = ByteArray(maxMessageSizeWithHeaders + 1) // Create message will fail. assertThatThrownBy { amqpClient.createMessage(largeData, sendAddress, CHARLIE_NAME.toString(), testProperty) @@ -393,7 +398,7 @@ class ProtonWrapperTests { } @Test(timeout=300_000) - fun `Message sent from AMQP to non-existent Artemis inbox is rejected and client disconnects`() { + fun `Message sent from AMQP to non-existent Artemis inbox is marked as acknowledged to avoid infinite retries`() { val (server, artemisClient) = createArtemisServerAndClient() val amqpClient = createClient() // AmqpClient is set to auto-reconnect, there might be multiple connect/disconnect rounds @@ -413,8 +418,9 @@ class ProtonWrapperTests { testProperty["TestProp"] = "1" val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) amqpClient.write(message) - assertEquals(MessageStatus.Rejected, message.onComplete.get()) - assertTrue(connectedStack.contains(false)) + assertEquals(MessageStatus.Acknowledged, message.onComplete.get()) + assertTrue(connectedStack.contains(true)) + assertEquals(1, connectedStack.size) amqpClient.stop() artemisClient.stop() server.stop() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt index 98a518d7bc..3b59ffa55c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt @@ -31,6 +31,7 @@ import java.security.PublicKey import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertNull +import kotlin.test.assertNotNull class CertificateRotationTest { private val ref = OpaqueBytes.of(0x01) @@ -180,7 +181,7 @@ class CertificateRotationTest { advertiseNodesToNetwork(mockNet.defaultNotaryNode, bob2, charlie) - assertNull(bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + assertNotNull(bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) assertNull(charlie.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) bob2.services.startFlow(CashPaymentFlow(1000.DOLLARS, charlie.party, false)) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 56af87c83f..cf9d022bff 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -183,6 +183,7 @@ class ArtemisMessagingTest { messagingClient.send(tooLagerMessage, messagingClient.myAddress) }.isInstanceOf(ActiveMQConnectionTimedOutException::class.java) assertNull(receivedMessages.poll(200, MILLISECONDS)) + this.messagingClient = null } @Test(timeout=300_000) @@ -232,7 +233,9 @@ class ArtemisMessagingTest { MetricRegistry(), TestingNamedCacheFactory(), isDrainingModeOn = { false }, - drainingModeWasChangedEvents = PublishSubject.create>()).apply { + drainingModeWasChangedEvents = PublishSubject.create>(), + terminateOnConnectionError = false, + timeoutConfig = P2PMessagingClient.TimeoutConfig(10.seconds, 10.seconds, 10.seconds)).apply { config.configureWithDevSSLCertificate() messagingClient = this } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/FlowManagerRPCOpsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/FlowManagerRPCOpsTest.kt new file mode 100644 index 0000000000..320db43d3c --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/FlowManagerRPCOpsTest.kt @@ -0,0 +1,67 @@ +@file:Suppress("DEPRECATION") +package net.corda.node.services.messaging + +import net.corda.client.rpc.ext.MultiRPCClient +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list +import net.corda.core.messaging.flows.FlowManagerRPCOps +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.node.internal.NodeStartup +import net.corda.node.services.Permissions +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import kotlin.test.assertNotNull +import net.corda.core.internal.messaging.FlowManagerRPCOps as InternalFlowManagerRPCOps + +class FlowManagerRPCOpsTest { + + @Test(timeout = 300_000) + fun `net_corda_core_internal_messaging_FlowManagerRPCOps can be accessed using the MultiRPCClient`() { + val user = User("user", "password", setOf(Permissions.all())) + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + val client = MultiRPCClient(nodeAHandle.rpcAddress, InternalFlowManagerRPCOps::class.java, user.username, user.password) + + val logDirPath = nodeAHandle.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME + logDirPath.createDirectories() + + client.use { + val rpcOps = it.start().getOrThrow(20.seconds).proxy + rpcOps.dumpCheckpoints() + it.stop() + } + + assertNotNull(logDirPath.list().singleOrNull { it.isRegularFile() }) + } + } + + @Test(timeout = 300_000) + fun `net_corda_core_messaging_flows_FlowManagerRPCOps can be accessed using the MultiRPCClient`() { + val user = User("user", "password", setOf(Permissions.all())) + driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + val client = MultiRPCClient(nodeAHandle.rpcAddress, FlowManagerRPCOps::class.java, user.username, user.password) + + val logDirPath = nodeAHandle.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME + logDirPath.createDirectories() + + client.use { + val rpcOps = it.start().getOrThrow(20.seconds).proxy + rpcOps.dumpCheckpoints() + it.stop() + } + + assertNotNull(logDirPath.list().singleOrNull { it.isRegularFile() }) + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt index c26a106910..139bb89505 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt @@ -4,6 +4,8 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.StateAndRef +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.CollectSignaturesFlow import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -12,20 +14,29 @@ import net.corda.core.flows.HospitalizeFlowException import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.NotaryException +import net.corda.core.flows.NotaryFlow import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.SignTransactionFlow import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract.SingleOwnerState +import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -33,6 +44,7 @@ import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.findCordapp import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before import org.junit.Test import java.sql.SQLException import java.util.* @@ -47,6 +59,12 @@ class FlowHospitalTest { private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) + @Before + fun before() { + SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow = false + CreateTransactionButDontFinalizeResponderFlow.exceptionSeenInUserFlow = false + } + @Test(timeout = 300_000) fun `when double spend occurs, the flow is successfully deleted on the counterparty`() { driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts")))) { @@ -172,7 +190,7 @@ class FlowHospitalTest { @Test(timeout = 300_000) fun `HospitalizeFlowException cloaking an important exception thrown`() { var dischargedCounter = 0 - var observationCounter: Int = 0 + var observationCounter = 0 StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargedCounter } @@ -197,6 +215,84 @@ class FlowHospitalTest { } } + @Test(timeout = 300_000) + fun `catching a notary error will cause a peer to fail with unexpected session end during ReceiveFinalityFlow that passes through user code`() { + var dischargedCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + val user = User("mark", "dadada", setOf(Permissions.all())) + driver(DriverParameters(isDebug = false, startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() + nodeAHandle.rpc.let { + val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + } + } + // 1 is the notary failing to notarise and propagating the error + // 2 is the receiving flow failing due to the unexpected session end error + assertEquals(2, dischargedCounter) + assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow) + } + + @Test(timeout = 300_000) + fun `unexpected session end errors outside of ReceiveFinalityFlow are not handled`() { + var dischargedCounter = 0 + var observationCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> + ++observationCounter + } + val user = User("mark", "dadada", setOf(Permissions.all())) + driver(DriverParameters(isDebug = false, startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeCHandle = startNode(providedName = CHARLIE_NAME, rpcUsers = listOf(user)).getOrThrow() + nodeAHandle.rpc.let { + val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds) + val ref2 = it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + val ref3 = it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeCHandle.nodeInfo.singleIdentity(), ref2).returnValue.getOrThrow(20.seconds) + it.startFlow(::CreateTransactionButDontFinalizeFlow, nodeBHandle.nodeInfo.singleIdentity(), ref3).returnValue.getOrThrow(20.seconds) + } + } + assertEquals(0, dischargedCounter) + assertEquals(1, observationCounter) + assertTrue(CreateTransactionButDontFinalizeResponderFlow.exceptionSeenInUserFlow) + } + + @Test(timeout = 300_000) + fun `unexpected session end errors within ReceiveFinalityFlow can be caught and the flow can end gracefully`() { + var dischargedCounter = 0 + var observationCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> + ++observationCounter + } + val user = User("mark", "dadada", setOf(Permissions.all())) + driver(DriverParameters(isDebug = false, startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() + nodeAHandle.rpc.let { + val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref, true).returnValue.getOrThrow(20.seconds) + } + } + // 1 is the notary failing to notarise and propagating the error + assertEquals(1, dischargedCounter) + assertEquals(0, observationCounter) + assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow) + } + @StartableByRPC class IssueFlow(val notary: Party) : FlowLogic>() { @@ -296,4 +392,136 @@ class FlowHospitalTest { setCause(SQLException("deadlock")) } } + + @InitiatingFlow + @StartableByRPC + class CreateTransactionFlow(private val peer: Party) : FlowLogic>() { + @Suspendable + override fun call(): StateAndRef { + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addOutputState(DummyState(participants = listOf(ourIdentity))) + addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey, peer.owningKey)) + } + val stx = serviceHub.signInitialTransaction(tx) + val session = initiateFlow(peer) + val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session))) + subFlow(FinalityFlow(ftx, session)) + + return ftx.coreTransaction.outRef(0) + } + } + + @InitiatedBy(CreateTransactionFlow::class) + class CreateTransactionResponderFlow(private val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + logger.info("CREATE TX - WAITING TO SIGN TX") + val stx = subFlow(object : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) { + + } + }) + logger.info("CREATE TX - SIGNED TO SIGN TX") + subFlow(ReceiveFinalityFlow(session, stx.id)) + logger.info("CREATE TX - RECEIVED TX") + } + } + + @InitiatingFlow + @StartableByRPC + class SpendStateAndCatchDoubleSpendFlow( + private val peer: Party, + private val ref: StateAndRef, + private val consumePeerError: Boolean + ) : FlowLogic>() { + + constructor(peer: Party, ref: StateAndRef): this(peer, ref, false) + + @Suspendable + override fun call(): StateAndRef { + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addInputState(ref) + addOutputState(DummyState(participants = listOf(ourIdentity))) + addCommand(DummyContract.Commands.Move(), listOf(ourIdentity.owningKey, peer.owningKey)) + } + val stx = serviceHub.signInitialTransaction(tx) + val session = initiateFlow(peer) + session.send(consumePeerError) + val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session))) + try { + subFlow(FinalityFlow(ftx, session)) + } catch(e: NotaryException) { + logger.info("Caught notary exception") + } + return ftx.coreTransaction.outRef(0) + } + } + + @InitiatedBy(SpendStateAndCatchDoubleSpendFlow::class) + class SpendStateAndCatchDoubleSpendResponderFlow(private val session: FlowSession) : FlowLogic() { + + companion object { + var exceptionSeenInUserFlow = false + } + + @Suspendable + override fun call() { + val consumeError = session.receive().unwrap { it } + val stx = subFlow(object : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) { + + } + }) + try { + subFlow(ReceiveFinalityFlow(session, stx.id)) + } catch (e: UnexpectedFlowEndException) { + exceptionSeenInUserFlow = true + if (!consumeError) { + throw e + } + } + } + } + + @InitiatingFlow + @StartableByRPC + class CreateTransactionButDontFinalizeFlow(private val peer: Party, private val ref: StateAndRef) : FlowLogic() { + + @Suspendable + override fun call() { + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addInputState(ref) + addOutputState(DummyState(participants = listOf(ourIdentity))) + addCommand(DummyContract.Commands.Move(), listOf(ourIdentity.owningKey)) + } + val stx = serviceHub.signInitialTransaction(tx) + val session = initiateFlow(peer) + // Send the transaction id to the peer instead of the transaction. + // This allows transaction dependency resolution to occur within the peer's [ReceiveTransactionFlow]. + session.send(stx.id) + // Mimic notarisation from [FinalityFlow] so that failing inside [ResolveTransactionsFlow] can be achieved. + val notarySignatures = subFlow(NotaryFlow.Client(stx, skipVerification = true)) + val notarisedTx = stx + notarySignatures + session.send(notarisedTx) + } + } + + @InitiatedBy(CreateTransactionButDontFinalizeFlow::class) + class CreateTransactionButDontFinalizeResponderFlow(private val session: FlowSession) : FlowLogic() { + + companion object { + var exceptionSeenInUserFlow = false + } + + @Suspendable + override fun call() { + val id = session.receive().unwrap { it } + try { + subFlow(ReceiveFinalityFlow(session, id)) + } catch (e: UnexpectedFlowEndException) { + exceptionSeenInUserFlow = true + throw e + } + } + } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 82c9804b8f..3b012b7672 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -28,6 +28,7 @@ import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -130,7 +131,11 @@ abstract class MQSecurityTest : NodeBasedTest() { fun assertTempQueueCreationAttackFails(queue: String) { assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") { - attacker.session.createTemporaryQueue(queue, RoutingType.MULTICAST, queue) + attacker.session.createQueue(QueueConfiguration(queue) + .setRoutingType(RoutingType.MULTICAST) + .setAddress(queue) + .setTemporary(true) + .setDurable(false)) } // Double-check assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy { @@ -147,7 +152,8 @@ abstract class MQSecurityTest : NodeBasedTest() { fun assertNonTempQueueCreationAttackFails(queue: String, durable: Boolean) { val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE" assertAttackFails(queue, permission) { - attacker.session.createQueue(queue, RoutingType.MULTICAST, queue, durable) + attacker.session.createQueue( + QueueConfiguration(queue).setAddress(queue).setRoutingType(RoutingType.MULTICAST).setDurable(durable)) } // Double-check assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy { 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 ff47272d64..3ff93abef5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -39,13 +39,13 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps -import net.corda.core.internal.messaging.FlowManagerRPCOps import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.rootMessage import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps +import net.corda.core.messaging.flows.FlowManagerRPCOps import net.corda.core.node.AppServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo @@ -84,6 +84,7 @@ import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.VirtualCordapp import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy +import net.corda.node.internal.shell.InteractiveShell import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler @@ -100,8 +101,6 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.rpc.NodeRpcOptions -import net.corda.node.services.config.shell.determineUnsafeUsers -import net.corda.node.services.config.shell.toShellConfig import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.diagnostics.NodeDiagnosticsService import net.corda.node.services.events.NodeSchedulerService @@ -168,7 +167,6 @@ import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess import net.corda.nodeapi.internal.namedThreadPoolExecutor -import net.corda.tools.shell.InteractiveShell import org.apache.activemq.artemis.utils.ReusableLatch import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig @@ -180,6 +178,7 @@ import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException +import java.util.ArrayList import java.util.Properties import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -401,23 +400,21 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } /** The implementation of the [RPCOps] interfaces used by this node. */ + @Suppress("DEPRECATION") open fun makeRPCOps(cordappLoader: CordappLoader): List { - val cordaRPCOpsImpl = Pair(CordaRPCOps::class.java, CordaRPCOpsImpl( - services, - smm, - flowStarter - ) { - shutdownExecutor.submit(::stop) - }.also { it.closeOnStop() }) + val cordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit(::stop) } + cordaRPCOps.closeOnStop() + val flowManagerRPCOps = FlowManagerRPCOpsImpl(checkpointDumper) + val attachmentTrustInfoRPCOps = AttachmentTrustInfoRPCOpsImpl(services.attachmentTrustCalculator) - val checkpointRPCOpsImpl = Pair(FlowManagerRPCOps::class.java, FlowManagerRPCOpsImpl(checkpointDumper)) - - val attachmentTrustInfoRPCOps = Pair(AttachmentTrustInfoRPCOps::class.java, AttachmentTrustInfoRPCOpsImpl(services.attachmentTrustCalculator)) - - return listOf(cordaRPCOpsImpl, checkpointRPCOpsImpl, attachmentTrustInfoRPCOps).map { rpcOpsImplPair -> + return listOf( + CordaRPCOps::class.java to cordaRPCOps, + FlowManagerRPCOps::class.java to flowManagerRPCOps, + net.corda.core.internal.messaging.FlowManagerRPCOps::class.java to flowManagerRPCOps, + AttachmentTrustInfoRPCOps::class.java to attachmentTrustInfoRPCOps + ).map { (targetInterface, implementation) -> // Mind that order of proxies is important - val targetInterface = rpcOpsImplPair.first - val stage1Proxy = AuthenticatedRpcOpsProxy.proxy(rpcOpsImplPair.second, targetInterface) + val stage1Proxy = AuthenticatedRpcOpsProxy.proxy(implementation, targetInterface) val stage2Proxy = ThreadContextAdjustingRpcOpsProxy.proxy(stage1Proxy, targetInterface, cordappLoader.appClassLoader) stage2Proxy @@ -448,7 +445,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - fun clearNetworkMapCache() { + open fun clearNetworkMapCache() { Node.printBasicNodeInfo("Clearing network map cache entries") log.info("Starting clearing of network map cache entries...") startDatabase() @@ -680,16 +677,18 @@ abstract class AbstractNode(val configuration: NodeConfiguration, open fun startShell() { if (configuration.shouldInitCrashShell()) { - val shellConfiguration = configuration.toShellConfig() - shellConfiguration.sshdPort?.let { - log.info("Binding Shell SSHD server on port $it.") + val isShellStarted = InteractiveShell.startShellIfInstalled(configuration, cordappLoader) + configuration.sshd?.port?.let { + if (isShellStarted) { + Node.printBasicNodeInfo("SSH server listening on port", configuration.sshd!!.port.toString()) + log.info("SSH server listening on port: $it.") + } else { + Node.printBasicNodeInfo( + "SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory" + ) + log.info("SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory") + } } - - val unsafeUsers = determineUnsafeUsers(configuration) - org.crsh.ssh.term.CRaSHCommand.setUserInfo(unsafeUsers, true, false) - log.info("Setting unsafe users as: ${unsafeUsers}") - - InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader) } } 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 bd3b6cb744..d83111487c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -566,6 +566,11 @@ open class Node(configuration: NodeConfiguration, return super.generateAndSaveNodeInfo() } + override fun clearNetworkMapCache() { + initialiseSerialization() + super.clearNetworkMapCache() + } + override fun runDatabaseMigrationScripts( updateCoreSchemas: Boolean, updateAppSchemas: Boolean, diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 95a1bf91c5..1772210e56 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -8,38 +8,56 @@ import net.corda.cliutils.printError import net.corda.common.logging.CordaVersion import net.corda.common.logging.errorReporting.CordaErrorContextProvider import net.corda.common.logging.errorReporting.ErrorCode +import net.corda.common.logging.errorReporting.ErrorReporting +import net.corda.common.logging.errorReporting.report import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.crypto.Crypto -import net.corda.core.internal.* +import net.corda.core.internal.Emoji +import net.corda.core.internal.HashAgility +import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div import net.corda.core.internal.errors.AddressBindingException +import net.corda.core.internal.exists +import net.corda.core.internal.isDirectory +import net.corda.core.internal.location +import net.corda.core.internal.randomOrNull +import net.corda.core.internal.safeSymbolicRead import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor -import net.corda.node.* -import net.corda.common.logging.errorReporting.ErrorReporting -import net.corda.common.logging.errorReporting.report +import net.corda.node.NodeCmdLineOptions +import net.corda.node.SerialFilter +import net.corda.node.SharedNodeCmdLineOptions +import net.corda.node.VersionInfo +import net.corda.node.defaultSerialFilter import net.corda.node.internal.Node.Companion.isInvalidJavaVersion import net.corda.node.internal.cordapp.MultipleCordappsForFlowException -import net.corda.node.internal.subcommands.* +import net.corda.node.internal.shell.InteractiveShell +import net.corda.node.internal.subcommands.ClearNetworkCacheCli +import net.corda.node.internal.subcommands.GenerateNodeInfoCli +import net.corda.node.internal.subcommands.GenerateRpcSslCertsCli +import net.corda.node.internal.subcommands.InitialRegistration +import net.corda.node.internal.subcommands.InitialRegistrationCli +import net.corda.node.internal.subcommands.RunMigrationScriptsCli +import net.corda.node.internal.subcommands.SynchroniseSchemasCli +import net.corda.node.internal.subcommands.ValidateConfigurationCli import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.shouldStartLocalShell -import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.utilities.registration.NodeRegistrationException import net.corda.nodeapi.internal.JVMAgentUtilities import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException -import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi import org.slf4j.bridge.SLF4JBridgeHandler import picocli.CommandLine.Mixin import java.io.IOException import java.io.RandomAccessFile -import java.lang.NullPointerException import java.lang.management.ManagementFactory import java.net.InetAddress import java.nio.channels.UnresolvedAddressException @@ -236,29 +254,21 @@ open class NodeStartup : NodeStartupLogging { val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded } logLoadedCorDapps(loadedCodapps) - node.nodeReadyFuture.thenMatch({ - // Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits. - val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 - val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation - Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") + node.nodeReadyFuture.thenMatch( + { + // Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits. + val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 + val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation + Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") - // Don't start the shell if there's no console attached. - if (node.configuration.shouldStartLocalShell()) { - node.startupComplete.then { - try { - InteractiveShell.runLocalShell(node::stop) - } catch (e: Exception) { - logger.error("Shell failed to start", e) - } + // Don't start the shell if there's no console attached. + if (node.configuration.shouldStartLocalShell()) { + InteractiveShell.runLocalShellIfInstalled(node::stop) } - } - if (node.configuration.shouldStartSSHDaemon()) { - Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString()) - } - }, - { th -> - logger.error("Unexpected exception during registration", th) - }) + }, + { th -> + logger.error("Unexpected exception during registration", th) + }) node.run() } diff --git a/node/src/main/kotlin/net/corda/node/internal/artemis/ArtemisBroker.kt b/node/src/main/kotlin/net/corda/node/internal/artemis/ArtemisBroker.kt index 5dc7e2c4ea..ccea39accf 100644 --- a/node/src/main/kotlin/net/corda/node/internal/artemis/ArtemisBroker.kt +++ b/node/src/main/kotlin/net/corda/node/internal/artemis/ArtemisBroker.kt @@ -1,6 +1,5 @@ package net.corda.node.internal.artemis -import io.netty.channel.unix.Errors import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.LifecycleSupport import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -18,4 +17,11 @@ data class BrokerAddresses(val primary: NetworkHostAndPort, private val adminArg val admin = adminArg ?: primary } -fun java.io.IOException.isBindingError() = this is BindException || this is Errors.NativeIoException && message?.contains("Address already in use") == true \ No newline at end of file +fun Throwable.isBindingError(): Boolean { + val addressAlreadyUsedMsg = "Address already in use" + // This is not an exact science here. + // Depending on the underlying OS it can be either [Errors.NativeIoException] on Linux or [BindException] on Windows + // and of course this is dependent on the version of Artemis library used. + return this is BindException || + this is IllegalStateException && cause.let { it is BindException || it?.message?.contains(addressAlreadyUsedMsg) == true } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt b/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt index 9658fe2e53..4038a0f2ef 100644 --- a/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt +++ b/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt @@ -14,6 +14,7 @@ import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal import java.io.IOException import java.security.KeyStore import java.security.Principal +import java.security.cert.X509Certificate import java.util.* import javax.security.auth.Subject import javax.security.auth.callback.CallbackHandler @@ -119,19 +120,18 @@ class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() { // The Main authentication logic, responsible for running all the configured checks for each user type // and return the actual User and principals - @Suppress("DEPRECATION") // should use java.security.cert.X509Certificate - private fun authenticateAndAuthorise(username: String, certificates: Array?, password: String): Pair> { - fun requireTls(certificates: Array?) = requireNotNull(certificates) { "No client certificates presented." } + private fun authenticateAndAuthorise(username: String, certificates: Array, password: String): Pair> { + fun requireTls(certificates: Array?) = requireNotNull(certificates) { "No client certificates presented." } return when (username) { ArtemisMessagingComponent.NODE_P2P_USER -> { requireTls(certificates) - CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!) + CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates) Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(NODE_P2P_ROLE))) } ArtemisMessagingComponent.NODE_RPC_USER -> { requireTls(certificates) - CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!) + CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates) Pair(ArtemisMessagingComponent.NODE_RPC_USER, listOf(RolePrincipal(NODE_RPC_ROLE))) } ArtemisMessagingComponent.PEER_USER -> { @@ -140,7 +140,7 @@ class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() { // This check is redundant as it was performed already during the SSL handshake CertificateChainCheckPolicy.RootMustMatch .createCheck(p2pJaasConfig.keyStore, p2pJaasConfig.trustStore) - .checkCertificateChain(certificates!!) + .checkCertificateChain(certificates) Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(PEER_ROLE))) } else -> { @@ -176,8 +176,8 @@ abstract class BaseBrokerJaasLoginModule : LoginModule { protected lateinit var callbackHandler: CallbackHandler protected val principals = ArrayList() - @Suppress("DEPRECATION") // should use java.security.cert.X509Certificate - protected fun getUsernamePasswordAndCerts(): Triple?> { + @Suppress("ThrowsCount") + protected fun getUsernamePasswordAndCerts(): Triple> { val nameCallback = NameCallback("Username: ") val passwordCallback = PasswordCallback("Password: ", false) val certificateCallback = CertificateCallback() diff --git a/node/src/main/kotlin/net/corda/node/internal/artemis/CertificateChainCheckPolicy.kt b/node/src/main/kotlin/net/corda/node/internal/artemis/CertificateChainCheckPolicy.kt index de1ac38bc8..6a4a77f071 100644 --- a/node/src/main/kotlin/net/corda/node/internal/artemis/CertificateChainCheckPolicy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/artemis/CertificateChainCheckPolicy.kt @@ -13,7 +13,7 @@ sealed class CertificateChainCheckPolicy { @FunctionalInterface interface Check { - fun checkCertificateChain(theirChain: Array) + fun checkCertificateChain(theirChain: Array) } abstract fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check @@ -21,7 +21,7 @@ sealed class CertificateChainCheckPolicy { object Any : CertificateChainCheckPolicy() { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { return object : Check { - override fun checkCertificateChain(theirChain: Array) { + override fun checkCertificateChain(theirChain: Array) { // nothing to do here } } @@ -33,7 +33,7 @@ sealed class CertificateChainCheckPolicy { val rootAliases = trustStore.aliases().asSequence().filter { it.startsWith(X509Utilities.CORDA_ROOT_CA) } val rootPublicKeys = rootAliases.map { trustStore.getCertificate(it).publicKey }.toSet() return object : Check { - override fun checkCertificateChain(theirChain: Array) { + override fun checkCertificateChain(theirChain: Array) { val theirRoot = theirChain.last().publicKey if (theirRoot !in rootPublicKeys) { throw CertificateException("Root certificate mismatch, their root = $theirRoot") @@ -47,7 +47,7 @@ sealed class CertificateChainCheckPolicy { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { val ourPublicKey = keyStore.getCertificate(X509Utilities.CORDA_CLIENT_TLS).publicKey return object : Check { - override fun checkCertificateChain(theirChain: Array) { + override fun checkCertificateChain(theirChain: Array) { val theirLeaf = theirChain.first().publicKey if (ourPublicKey != theirLeaf) { throw CertificateException("Leaf certificate mismatch, their leaf = $theirLeaf") @@ -61,7 +61,7 @@ sealed class CertificateChainCheckPolicy { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { val trustedPublicKeys = trustedAliases.map { trustStore.getCertificate(it).publicKey }.toSet() return object : Check { - override fun checkCertificateChain(theirChain: Array) { + override fun checkCertificateChain(theirChain: Array) { if (!theirChain.any { it.publicKey in trustedPublicKeys }) { throw CertificateException("Their certificate chain contained none of the trusted ones") } @@ -78,7 +78,7 @@ sealed class CertificateChainCheckPolicy { class UsernameMustMatchCommonNameCheck : Check { lateinit var username: String - override fun checkCertificateChain(theirChain: Array) { + override fun checkCertificateChain(theirChain: Array) { if (!theirChain.any { certificate -> CordaX500Name.parse(certificate.subjectDN.name).commonName == username }) { throw CertificateException("Client certificate does not match login username.") } diff --git a/node/src/main/kotlin/net/corda/node/internal/checkpoints/FlowManagerRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/checkpoints/FlowManagerRPCOpsImpl.kt index 04f83d1426..36545dfb7d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/checkpoints/FlowManagerRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/checkpoints/FlowManagerRPCOpsImpl.kt @@ -1,13 +1,15 @@ +@file:Suppress("DEPRECATION") package net.corda.node.internal.checkpoints import net.corda.core.internal.PLATFORM_VERSION -import net.corda.core.internal.messaging.FlowManagerRPCOps +import net.corda.core.messaging.flows.FlowManagerRPCOps import net.corda.node.services.rpc.CheckpointDumperImpl +import net.corda.core.internal.messaging.FlowManagerRPCOps as InternalFlowManagerRPCOps /** * Implementation of [FlowManagerRPCOps] */ -internal class FlowManagerRPCOpsImpl(private val checkpointDumper: CheckpointDumperImpl) : FlowManagerRPCOps { +internal class FlowManagerRPCOpsImpl(private val checkpointDumper: CheckpointDumperImpl) : FlowManagerRPCOps, InternalFlowManagerRPCOps { override val protocolVersion: Int = PLATFORM_VERSION diff --git a/node/src/main/kotlin/net/corda/node/internal/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/internal/shell/InteractiveShell.kt new file mode 100644 index 0000000000..7952789813 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/shell/InteractiveShell.kt @@ -0,0 +1,87 @@ +package net.corda.node.internal.shell + +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.shell.determineUnsafeUsers +import net.corda.node.services.config.shell.toShellConfigMap +import net.corda.nodeapi.internal.cordapp.CordappLoader +import org.slf4j.LoggerFactory + +object InteractiveShell { + + private val log = LoggerFactory.getLogger(InteractiveShell::class.java) + + private const val INTERACTIVE_SHELL_CLASS = "net.corda.tools.shell.InteractiveShell" + private const val CRASH_COMMAND_CLASS = "org.crsh.ssh.term.CRaSHCommand" + + private const val START_SHELL_METHOD = "startShell" + private const val RUN_LOCAL_SHELL_METHOD = "runLocalShell" + private const val SET_USER_INFO_METHOD = "setUserInfo" + + fun startShellIfInstalled(configuration: NodeConfiguration, cordappLoader: CordappLoader): Boolean { + return if (isShellInstalled()) { + try { + val shellConfiguration = configuration.toShellConfigMap() + setUnsafeUsers(configuration) + startShell(shellConfiguration, cordappLoader) + true + } catch (e: Exception) { + log.error("Shell failed to start", e) + false + } + } else { + false + } + } + + /** + * Only call this after [startShellIfInstalled] has been called or the required classes will not be loaded into the current classloader. + */ + fun runLocalShellIfInstalled(onExit: () -> Unit = {}): Boolean { + return if (isShellInstalled()) { + try { + runLocalShell(onExit) + true + } catch (e: Exception) { + log.error("Shell failed to start", e) + false + } + } else { + false + } + } + + private fun isShellInstalled(): Boolean { + return try { + javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS) + true + } catch (e: ClassNotFoundException) { + false + } + } + + private fun setUnsafeUsers(configuration: NodeConfiguration) { + val unsafeUsers = determineUnsafeUsers(configuration) + val clazz = javaClass.classLoader.loadClass(CRASH_COMMAND_CLASS) + clazz.getDeclaredMethod(SET_USER_INFO_METHOD, Set::class.java, Boolean::class.java, Boolean::class.java) + .invoke(null, unsafeUsers, true, false) + log.info("Setting unsafe users as: $unsafeUsers") + } + + private fun startShell(shellConfiguration: Map, cordappLoader: CordappLoader) { + val clazz = javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS) + val instance = clazz.getDeclaredConstructor() + .apply { this.isAccessible = true } + .newInstance() + clazz.getDeclaredMethod(START_SHELL_METHOD, Map::class.java, ClassLoader::class.java, Boolean::class.java) + .invoke(instance, shellConfiguration, cordappLoader.appClassLoader, false) + } + + private fun runLocalShell(onExit: () -> Unit = {}) { + val clazz = javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS) + // Gets the existing instance created by [startShell] as [InteractiveShell] is a static instance + val instance = clazz.getDeclaredConstructor() + .apply { this.isAccessible = true } + .newInstance() + clazz.getDeclaredMethod(RUN_LOCAL_SHELL_METHOD, Function0::class.java).invoke(instance, onExit) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt index b765685910..28d8dc3a89 100644 --- a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt @@ -13,6 +13,7 @@ import net.corda.node.internal.DBNetworkParametersStorage import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.vault.NodeVaultService @@ -132,7 +133,8 @@ object VaultMigrationSchemaV1 : MappedSchema(schemaFamily = VaultMigrationSchema PersistentIdentityService.PersistentPublicKeyHashToParty::class.java, BasicHSMKeyManagementService.PersistentKey::class.java, NodeAttachmentService.DBAttachment::class.java, - DBNetworkParametersStorage.PersistentNetworkParameters::class.java + DBNetworkParametersStorage.PersistentNetworkParameters::class.java, + PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java ) ) diff --git a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt index c77e39cec0..2bed926da5 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt @@ -15,4 +15,6 @@ interface IdentityServiceInternal : IdentityService { fun verifyAndRegisterNewRandomIdentity(identity: PartyAndCertificate) fun invalidateCaches(name: CordaX500Name) {} + + fun archiveNamedIdentity(name:String, publicKeyHash: String?) {} } \ No newline at end of file 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 bdda67f19a..21ed0db28b 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 @@ -11,17 +11,18 @@ import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec +import net.corda.node.services.config.shell.SSHDConfiguration import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.raft.RaftConfig -import net.corda.tools.shell.SSHDConfiguration import java.net.URL import java.nio.file.Path import java.time.Duration -import java.util.* +import java.util.Properties +import java.util.UUID import javax.security.auth.x500.X500Principal val Int.MB: Long get() = this * 1024L * 1024L diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index 49390958bf..c6efe102ea 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.services.config.rpc.NodeRpcOptions +import net.corda.node.services.config.shell.SSHDConfiguration import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier @@ -15,11 +16,11 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.tools.shell.SSHDConfiguration import java.net.URL import java.nio.file.Path import java.time.Duration -import java.util.* +import java.util.Properties +import java.util.UUID import javax.security.auth.x500.X500Principal data class NodeConfigurationImpl( 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 eb06334901..79a9c81dea 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 @@ -40,13 +40,14 @@ import net.corda.node.services.config.schema.parsers.toProperties import net.corda.node.services.config.schema.parsers.toURL import net.corda.node.services.config.schema.parsers.toUUID import net.corda.node.services.config.schema.parsers.validValue +import net.corda.node.services.config.shell.SSHDConfiguration import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.raft.RaftConfig -import net.corda.tools.shell.SSHDConfiguration +import java.util.Properties internal object UserSpec : Configuration.Specification("User") { private val username by string().optional() @@ -67,9 +68,32 @@ internal object UserSpec : Configuration.Specification("User") { internal object SecurityConfigurationSpec : Configuration.Specification("SecurityConfiguration") { private object AuthServiceSpec : Configuration.Specification("AuthService") { private object DataSourceSpec : Configuration.Specification("DataSource") { + fun Properties.enablePasswordMasking(): Properties { + class PwMasking : Properties() { + fun maskPassword(): Properties { + if (!containsKey("password")) return this + val propsNoPassword = Properties() + // if the properties are passed in to the constructor as defaults + // they don't get printed so adding all keys explicitly + propsNoPassword.putAll(this) + propsNoPassword.setProperty("password", "***") + return propsNoPassword + } + + override fun toString(): String { + val props = maskPassword() + return props.toString() + } + } + + val masker = PwMasking() + masker.putAll(this) + return masker + } + private val type by enum(AuthDataSourceType::class) private val passwordEncryption by enum(PasswordEncryption::class).optional().withDefaultValue(SecurityConfiguration.AuthService.DataSource.Defaults.passwordEncryption) - private val connection by nestedObject(sensitive = true).map(::toProperties).optional() + private val connection by nestedObject(sensitive = true).map{ toProperties(it).enablePasswordMasking() }.optional() private val users by nested(UserSpec).list().optional() override fun parseValid(configuration: Config, options: Configuration.Options): Valid { diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/SSHDConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/SSHDConfiguration.kt similarity index 87% rename from tools/shell/src/main/kotlin/net/corda/tools/shell/SSHDConfiguration.kt rename to node/src/main/kotlin/net/corda/node/services/config/shell/SSHDConfiguration.kt index 82800e5ab1..fe05b9f479 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/SSHDConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/SSHDConfiguration.kt @@ -1,4 +1,4 @@ -package net.corda.tools.shell +package net.corda.node.services.config.shell data class SSHDConfiguration(val port: Int) { companion object { @@ -11,7 +11,7 @@ data class SSHDConfiguration(val port: Int) { */ @JvmStatic fun parse(str: String): SSHDConfiguration { - require(!str.isBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) } + require(str.isNotBlank()) { MISSING_PORT_FORMAT.format(str) } val port = try { str.toInt() } catch (ex: NumberFormatException) { diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt index 200506a2a5..1f89c8e01f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt @@ -3,22 +3,23 @@ package net.corda.node.services.config.shell import net.corda.core.internal.div import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.services.config.NodeConfiguration -import net.corda.tools.shell.ShellConfiguration -import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR -import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR -import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR + +private const val COMMANDS_DIR = "shell-commands" +private const val CORDAPPS_DIR = "cordapps" +private const val SSHD_HOSTKEY_DIR = "ssh" //re-packs data to Shell specific classes -fun NodeConfiguration.toShellConfig() = ShellConfiguration( - commandsDirectory = this.baseDirectory / COMMANDS_DIR, - cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, - user = INTERNAL_SHELL_USER, - password = internalShellPassword, - permissions = internalShellPermissions(!this.localShellUnsafe), - localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode, - localShellUnsafe = this.localShellUnsafe, - hostAndPort = this.rpcOptions.address, - ssl = clientSslOptionsCompatibleWith(this.rpcOptions), - sshdPort = this.sshd?.port, - sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR, - noLocalShell = this.noLocalShell) +fun NodeConfiguration.toShellConfigMap() = mapOf( + "commandsDirectory" to this.baseDirectory / COMMANDS_DIR, + "cordappsDirectory" to this.baseDirectory.toString() / CORDAPPS_DIR, + "user" to INTERNAL_SHELL_USER, + "password" to internalShellPassword, + "permissions" to internalShellPermissions(!this.localShellUnsafe), + "localShellAllowExitInSafeMode" to this.localShellAllowExitInSafeMode, + "localShellUnsafe" to this.localShellUnsafe, + "hostAndPort" to this.rpcOptions.address, + "ssl" to clientSslOptionsCompatibleWith(this.rpcOptions), + "sshdPort" to this.sshd?.port, + "sshHostKeyDirectory" to this.baseDirectory / SSHD_HOSTKEY_DIR, + "noLocalShell" to this.noLocalShell +) diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 20e47fb2fe..2e10525141 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -1,5 +1,6 @@ package net.corda.node.services.identity +import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.crypto.Crypto import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty @@ -22,6 +23,7 @@ import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.NotaryUpdateListener +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap @@ -46,6 +48,8 @@ import java.security.cert.CollectionCertStoreParameters import java.security.cert.TrustAnchor import java.security.cert.X509Certificate import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import java.util.stream.Stream import javax.annotation.concurrent.ThreadSafe import javax.persistence.Column @@ -140,6 +144,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri private fun mapToKey(party: PartyAndCertificate) = party.owningKey.toStringShort() } + val archiveIdentityExecutor: ExecutorService = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("archive-named-identity-thread-%d").build()) + @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") class PersistentPublicKeyHashToCertificate( @@ -312,7 +318,76 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri } override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = database.transaction { - nameToParty[name]?.orElse(null) + if (nameToParty[name]?.isPresent == true) { + nameToParty[name]?.get() + } + else { + retrievePartyFromArchive(name) + } + } + + private fun retrievePartyFromArchive(name: CordaX500Name): Party? { + val hashKey = database.transaction { + val query = session.criteriaBuilder.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val queryRoot = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + query.where(session.criteriaBuilder.equal(queryRoot.get("name"), name.toString())) + val resultList = session.createQuery(query).resultList + if (resultList.isNotEmpty()) { + resultList?.first()?.publicKeyHash + } + else { + retrieveHashKeyAndCacheParty(name) + } + } + return hashKey?.let { keyToPartyAndCert[it]?.party } + } + + private fun retrieveHashKeyAndCacheParty(name: CordaX500Name): String? { + return database.transaction { + val cb = session.criteriaBuilder + val query = cb.createQuery(PersistentPublicKeyHashToParty::class.java) + val root = query.from(PersistentPublicKeyHashToParty::class.java) + val isNotConfidentialIdentity = cb.equal(root.get("publicKeyHash"), root.get("owningKeyHash")) + val matchName = cb.equal(root.get("name"), name.toString()) + query.select(root).where(cb.and(matchName, isNotConfidentialIdentity)) + val resultList = session.createQuery(query).resultList + var hashKey: String? = if (resultList.isNotEmpty()) { + if (resultList.size == 1) { + resultList?.single()?.owningKeyHash + } + else { + selectIdentityHash(session, resultList.mapNotNull { it.publicKeyHash }, name) + } + } else { + null + } + archiveNamedIdentity(name.toString(), hashKey) + hashKey + } + } + + private fun selectIdentityHash(session: Session, hashList: List, name: CordaX500Name): String? { + val cb = session.criteriaBuilder + val query = cb.createQuery(PersistentPublicKeyHashToCertificate::class.java) + val root = query.from(PersistentPublicKeyHashToCertificate::class.java) + query.select(root).where(root.get("publicKeyHash").`in`(hashList)) + val resultList = session.createQuery(query).resultList + resultList.sortWith(compareBy { PartyAndCertificate(X509CertificateFactory().delegate.generateCertPath(it.identity.inputStream())).certificate.notBefore }) + log.warn("Retrieving identity hash for removed identity '${name}', more that one hash found, returning last one according to notBefore validity of certificate." + + " Hash returned is ${resultList.last().publicKeyHash}") + return resultList.last().publicKeyHash + } + + override fun archiveNamedIdentity(name:String, publicKeyHash: String?) { + archiveIdentityExecutor.submit { + database.transaction { + val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val queryRoot = deleteQuery.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + deleteQuery.where(session.criteriaBuilder.equal(queryRoot.get("name"), name)) + session.createQuery(deleteQuery).executeUpdate() + session.save(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash(name, publicKeyHash)) + } + }.get() } override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index ed7a5b4cb6..c0a6ed0388 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -18,6 +18,7 @@ import net.corda.node.internal.artemis.SecureArtemisConfiguration import net.corda.node.internal.artemis.UserValidationPlugin import net.corda.node.internal.artemis.isBindingError import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.artemis.startSynchronously import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX @@ -41,7 +42,6 @@ import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager -import java.io.IOException import java.lang.Long.max import javax.annotation.concurrent.ThreadSafe import javax.security.auth.login.AppConfigurationEntry @@ -107,21 +107,21 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, val artemisConfig = createArtemisConfig() val securityManager = createArtemisSecurityManager() activeMQServer = ActiveMQServerImpl(artemisConfig, securityManager).apply { - // Throw any exceptions which are detected during startup - registerActivationFailureListener { exception -> throw exception } // Some types of queue might need special preparation on our side, like dialling back or preparing // a lazily initialised subsystem. registerPostQueueCreationCallback { log.debug { "Queue Created: $it" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } } + @Suppress("TooGenericExceptionCaught") try { - activeMQServer.start() - } catch (e: IOException) { + activeMQServer.startSynchronously() + } catch (e: Throwable) { log.error("Unable to start message broker", e) if (e.isBindingError()) { throw AddressBindingException(config.p2pAddress) } else { + log.error("Unexpected error starting message broker", e) throw e } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeNettyAcceptorFactory.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeNettyAcceptorFactory.kt index 1709f896dd..69809cafc6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeNettyAcceptorFactory.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeNettyAcceptorFactory.kt @@ -5,50 +5,37 @@ import io.netty.channel.ChannelHandlerContext import io.netty.channel.group.ChannelGroup import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler -import io.netty.handler.ssl.SslContext -import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslHandler import io.netty.handler.ssl.SslHandshakeTimeoutException -import io.netty.handler.ssl.SslProvider import net.corda.core.internal.declaredField import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace import net.corda.nodeapi.internal.ArtemisTcpTransport -import net.corda.nodeapi.internal.config.CertificateStore -import net.corda.nodeapi.internal.protonwrapper.netty.createAndInitSslContext -import net.corda.nodeapi.internal.protonwrapper.netty.keyManagerFactory import net.corda.nodeapi.internal.protonwrapper.netty.sslDelegatedTaskExecutor import net.corda.nodeapi.internal.setThreadPoolName -import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.BaseInterceptor import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor -import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants -import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport -import org.apache.activemq.artemis.core.server.ActiveMQServerLogger +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler import org.apache.activemq.artemis.core.server.cluster.ClusterConnection import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager import org.apache.activemq.artemis.spi.core.remoting.Acceptor import org.apache.activemq.artemis.spi.core.remoting.AcceptorFactory import org.apache.activemq.artemis.spi.core.remoting.BufferHandler import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener +import org.apache.activemq.artemis.spi.core.remoting.ssl.OpenSSLContextFactoryProvider +import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider import org.apache.activemq.artemis.utils.ConfigurationHelper import org.apache.activemq.artemis.utils.actors.OrderedExecutor import java.net.SocketAddress import java.nio.channels.ClosedChannelException -import java.nio.file.Paths -import java.security.PrivilegedExceptionAction import java.time.Duration import java.util.concurrent.Executor import java.util.concurrent.ScheduledExecutorService import java.util.regex.Pattern -import javax.net.ssl.KeyManagerFactory -import javax.net.ssl.SSLContext import javax.net.ssl.SSLEngine import javax.net.ssl.SSLPeerUnverifiedException -import javax.net.ssl.TrustManagerFactory -import javax.security.auth.Subject -@Suppress("unused", "TooGenericExceptionCaught", "ComplexMethod", "MagicNumber", "TooManyFunctions") +@Suppress("unused") // Used via reflection in ArtemisTcpTransport class NodeNettyAcceptorFactory : AcceptorFactory { override fun createAcceptor(name: String?, clusterConnection: ClusterConnection?, @@ -57,7 +44,7 @@ class NodeNettyAcceptorFactory : AcceptorFactory { listener: ServerConnectionLifeCycleListener?, threadPool: Executor, scheduledThreadPool: ScheduledExecutorService, - protocolMap: Map>>?): Acceptor { + protocolMap: MutableMap, RedirectHandler<*>>>?): Acceptor { val threadPoolName = ConfigurationHelper.getStringProperty(ArtemisTcpTransport.THREAD_POOL_NAME_NAME, "Acceptor", configuration) threadPool.setThreadPoolName("$threadPoolName-artemis") scheduledThreadPool.setThreadPoolName("$threadPoolName-artemis-scheduler") @@ -83,12 +70,18 @@ class NodeNettyAcceptorFactory : AcceptorFactory { listener: ServerConnectionLifeCycleListener?, scheduledThreadPool: ScheduledExecutorService?, failureExecutor: Executor, - protocolMap: Map>>?, + protocolMap: MutableMap, RedirectHandler<*>>>?, private val threadPoolName: String) : NettyAcceptor(name, clusterConnection, configuration, handler, listener, scheduledThreadPool, failureExecutor, protocolMap) { companion object { private val defaultThreadPoolNamePattern = Pattern.compile("""Thread-(\d+) \(activemq-netty-threads\)""") + + init { + // Make sure Artemis isn't using another (Open)SSLContextFactory + check(SSLContextFactoryProvider.getSSLContextFactory() is NodeSSLContextFactory) + check(OpenSSLContextFactoryProvider.getOpenSSLContextFactory() is NodeOpenSSLContextFactory) + } } private val sslDelegatedTaskExecutor = sslDelegatedTaskExecutor(threadPoolName) @@ -112,9 +105,9 @@ class NodeNettyAcceptorFactory : AcceptorFactory { } @Synchronized - override fun getSslHandler(alloc: ByteBufAllocator?): SslHandler { + override fun getSslHandler(alloc: ByteBufAllocator?, peerHost: String?, peerPort: Int): SslHandler { applyThreadPoolName() - val engine = getSSLEngine(alloc) + val engine = super.getSslHandler(alloc, peerHost, peerPort).engine() val sslHandler = NodeAcceptorSslHandler(engine, sslDelegatedTaskExecutor, trace) val handshakeTimeout = configuration[ArtemisTcpTransport.SSL_HANDSHAKE_TIMEOUT_NAME] as Duration? if (handshakeTimeout != null) { @@ -132,111 +125,6 @@ class NodeNettyAcceptorFactory : AcceptorFactory { Thread.currentThread().name = "$threadPoolName-${matcher.group(1)}" // Preserve the pool thread number } } - - /** - * This is a copy of [NettyAcceptor.getSslHandler] so that we can provide different implementations for [loadOpenSslEngine] and - * [loadJdkSslEngine]. [NodeNettyAcceptor], instead of creating a default [TrustManagerFactory], will simply use the provided one in - * the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] configuration. - */ - private fun getSSLEngine(alloc: ByteBufAllocator?): SSLEngine { - val engine = if (sslProvider == TransportConstants.OPENSSL_PROVIDER) { - loadOpenSslEngine(alloc) - } else { - loadJdkSslEngine() - } - engine.useClientMode = false - if (needClientAuth) { - engine.needClientAuth = true - } - - // setting the enabled cipher suites resets the enabled protocols so we need - // to save the enabled protocols so that after the customer cipher suite is enabled - // we can reset the enabled protocols if a customer protocol isn't specified - val originalProtocols = engine.enabledProtocols - if (enabledCipherSuites != null) { - try { - engine.enabledCipherSuites = SSLSupport.parseCommaSeparatedListIntoArray(enabledCipherSuites) - } catch (e: IllegalArgumentException) { - ActiveMQServerLogger.LOGGER.invalidCipherSuite(SSLSupport.parseArrayIntoCommandSeparatedList(engine.supportedCipherSuites)) - throw e - } - } - if (enabledProtocols != null) { - try { - engine.enabledProtocols = SSLSupport.parseCommaSeparatedListIntoArray(enabledProtocols) - } catch (e: IllegalArgumentException) { - ActiveMQServerLogger.LOGGER.invalidProtocol(SSLSupport.parseArrayIntoCommandSeparatedList(engine.supportedProtocols)) - throw e - } - } else { - engine.enabledProtocols = originalProtocols - } - return engine - } - - /** - * Copy of [NettyAcceptor.loadOpenSslEngine] which invokes our custom [createOpenSslContext]. - */ - private fun loadOpenSslEngine(alloc: ByteBufAllocator?): SSLEngine { - val context = try { - // We copied all this code just so we could replace the SSLSupport.createNettyContext method call with our own one. - createOpenSslContext() - } catch (e: Exception) { - throw IllegalStateException("Unable to create NodeNettyAcceptor", e) - } - return Subject.doAs(null, PrivilegedExceptionAction { - context.newEngine(alloc) - }) - } - - /** - * Copy of [NettyAcceptor.loadJdkSslEngine] which invokes our custom [createJdkSSLContext]. - */ - private fun loadJdkSslEngine(): SSLEngine { - val context = try { - // We copied all this code just so we could replace the SSLHelper.createContext method call with our own one. - createJdkSSLContext() - } catch (e: Exception) { - throw IllegalStateException("Unable to create NodeNettyAcceptor", e) - } - return Subject.doAs(null, PrivilegedExceptionAction { - context.createSSLEngine() - }) - } - - /** - * Create an [SSLContext] using the [TrustManagerFactory] provided on the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] config. - */ - private fun createJdkSSLContext(): SSLContext { - return createAndInitSslContext( - createKeyManagerFactory(), - configuration[ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory? - ) - } - - /** - * Create an [SslContext] using the the [TrustManagerFactory] provided on the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] config. - */ - private fun createOpenSslContext(): SslContext { - return SslContextBuilder - .forServer(createKeyManagerFactory()) - .sslProvider(SslProvider.OPENSSL) - .trustManager(configuration[ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory?) - .build() - } - - private fun createKeyManagerFactory(): KeyManagerFactory { - return keyManagerFactory(CertificateStore.fromFile(Paths.get(keyStorePath), keyStorePassword, keyStorePassword, false)) - } - - // Replicate the fields which are private in NettyAcceptor - private val sslProvider = ConfigurationHelper.getStringProperty(TransportConstants.SSL_PROVIDER, TransportConstants.DEFAULT_SSL_PROVIDER, configuration) - private val needClientAuth = ConfigurationHelper.getBooleanProperty(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, TransportConstants.DEFAULT_NEED_CLIENT_AUTH, configuration) - private val enabledCipherSuites = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES, configuration) - private val enabledProtocols = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, TransportConstants.DEFAULT_ENABLED_PROTOCOLS, configuration) - private val keyStorePath = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PATH_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PATH, configuration) - private val keyStoreProvider = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PROVIDER, configuration) - private val keyStorePassword = ConfigurationHelper.getPasswordProperty(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PASSWORD, configuration, ActiveMQDefaultConfiguration.getPropMaskPassword(), ActiveMQDefaultConfiguration.getPropPasswordCodec()) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeSSLContextFactory.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeSSLContextFactory.kt new file mode 100644 index 0000000000..38f60d7a57 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeSSLContextFactory.kt @@ -0,0 +1,59 @@ +package net.corda.node.services.messaging + +import io.netty.handler.ssl.SslContext +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.ssl.SslProvider +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.TRUST_MANAGER_FACTORY_NAME +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.protonwrapper.netty.createAndInitSslContext +import net.corda.nodeapi.internal.protonwrapper.netty.keyManagerFactory +import org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultOpenSSLContextFactory +import org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultSSLContextFactory +import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextConfig +import java.nio.file.Paths +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + +class NodeSSLContextFactory : DefaultSSLContextFactory() { + override fun getSSLContext(config: SSLContextConfig, additionalOpts: Map): SSLContext { + val trustManagerFactory = additionalOpts[TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory? + return if (trustManagerFactory != null) { + createAndInitSslContext(loadKeyManagerFactory(config), trustManagerFactory) + } else { + super.getSSLContext(config, additionalOpts) + } + } + + override fun getPriority(): Int { + // We make sure this factory is the one that's chosen, so any sufficiently large value will do. + return 15 + } +} + + +class NodeOpenSSLContextFactory : DefaultOpenSSLContextFactory() { + override fun getServerSslContext(config: SSLContextConfig, additionalOpts: Map): SslContext { + val trustManagerFactory = additionalOpts[TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory? + return if (trustManagerFactory != null) { + SslContextBuilder + .forServer(loadKeyManagerFactory(config)) + .sslProvider(SslProvider.OPENSSL) + .trustManager(trustManagerFactory) + .build() + } else { + super.getServerSslContext(config, additionalOpts) + } + } + + override fun getPriority(): Int { + // We make sure this factory is the one that's chosen, so any sufficiently large value will do. + return 15 + } +} + + +private fun loadKeyManagerFactory(config: SSLContextConfig): KeyManagerFactory { + val keyStore = CertificateStore.fromFile(Paths.get(config.keystorePath), config.keystorePassword, config.keystorePassword, false) + return keyManagerFactory(keyStore) +} diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt index 4a63927fad..bb95590de6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt @@ -24,6 +24,10 @@ class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val databa private val beingProcessedMessages = ConcurrentHashMap() private val processedMessages = createProcessedMessages(cacheFactory) + enum class Outcome { + NEW, DUPLICATE, IN_FLIGHT + } + private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( cacheFactory = cacheFactory, @@ -48,15 +52,17 @@ class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val databa private fun senderHash(senderKey: SenderKey) = SecureHash.sha256(senderKey.peer.toString() + senderKey.isSessionInit.toString() + senderKey.senderUUID).toString() /** - * @return true if we have seen this message before. + * @return IN_FLIGHT if this message is currently being processed by the state machine, otherwise indicate if DUPLICATE or NEW. */ - fun isDuplicate(msg: ReceivedMessage): Boolean { + fun checkDuplicate(msg: ReceivedMessage): Outcome { if (beingProcessedMessages.containsKey(msg.uniqueMessageId)) { - return true + return Outcome.IN_FLIGHT } - return isDuplicateInDatabase(msg) + return booleanToEnum(isDuplicateInDatabase(msg)) } + private fun booleanToEnum(isDuplicate: Boolean): Outcome = if (isDuplicate) Outcome.DUPLICATE else Outcome.NEW + /** * Called the first time we encounter [deduplicationId]. */ 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 b9caae56aa..ea132130f1 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 @@ -18,7 +18,15 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.serialize -import net.corda.core.utilities.* +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.minutes +import net.corda.core.utilities.seconds +import net.corda.core.utilities.trace import net.corda.node.VersionInfo import net.corda.node.internal.LifecycleSupport import net.corda.node.internal.artemis.ReactiveArtemisConsumer @@ -31,12 +39,15 @@ import net.corda.node.services.statemachine.SenderDeduplicationId import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.errorAndTerminate import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.ArtemisMessagingComponent.* +import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress 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.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress +import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress +import net.corda.nodeapi.internal.ArtemisMessagingComponent.ServiceAddress import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.bridging.BridgeControl import net.corda.nodeapi.internal.bridging.BridgeEntry @@ -48,15 +59,26 @@ import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString -import org.apache.activemq.artemis.api.core.client.* +import org.apache.activemq.artemis.api.core.client.ActiveMQClient +import org.apache.activemq.artemis.api.core.client.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.apache.activemq.artemis.api.core.client.ClientSessionFactory +import org.apache.activemq.artemis.api.core.client.FailoverEventType +import org.apache.activemq.artemis.api.core.client.ServerLocator import rx.Observable import rx.Subscription import rx.subjects.PublishSubject import java.security.PublicKey +import java.time.Duration import java.time.Instant -import java.util.* +import java.util.Collections +import java.util.Timer +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CountDownLatch import javax.annotation.concurrent.ThreadSafe @@ -72,15 +94,17 @@ import kotlin.concurrent.timer * executor through into Artemis and from there, back through to senders. * * An implementation of [CordaRPCOps] can be provided. If given, clients using the CordaMQClient RPC library can - * invoke methods on the provided implementation. There is more documentation on this in the docsite and the + * invoke methods on the provided implementation. There is more documentation on this in the doc-site and the * CordaRPCClient class. * * @param config The configuration of the node, which is used for controlling the message redelivery options. * @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility. * @param serverAddress The host and port of the Artemis broker. * @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends. - * @param database The nodes database, which is used to deduplicate messages. + * @param database The node's database, which is used to deduplicate messages. + * @param terminateOnConnectionError whether the process should be terminated forcibly if connection with the broker fails. */ +@Suppress("LongParameterList") @ThreadSafe class P2PMessagingClient(val config: NodeConfiguration, private val versionInfo: VersionInfo, @@ -94,7 +118,9 @@ class P2PMessagingClient(val config: NodeConfiguration, private val isDrainingModeOn: () -> Boolean, private val drainingModeWasChangedEvents: Observable>, private val threadPoolName: String = "P2PClient", - private val stateHelper: ServiceStateHelper = ServiceStateHelper(log) + private val stateHelper: ServiceStateHelper = ServiceStateHelper(log), + private val terminateOnConnectionError: Boolean = true, + private val timeoutConfig: TimeoutConfig = TimeoutConfig.default() ) : SingletonSerializeAsToken(), MessagingService, AddressToArtemisQueueResolver, ServiceStateSupport by stateHelper { companion object { private val log = contextLogger() @@ -127,6 +153,21 @@ class P2PMessagingClient(val config: NodeConfiguration, fun sendMessage(address: String, message: ClientMessage) = producer!!.send(address, message) } + /** + * @property callTimeout the time a blocking call (e.g. message send) from a client waits for a response until it times out. + * @property serverConnectionTtl the time the server waits for a packet/heartbeat from a client before it announces the connection dead and cleans it up. + * @property clientConnectionTtl the time the client waits for a packet/heartbeat from a client before it announces the connection dead and cleans it up. + */ + data class TimeoutConfig(val callTimeout: Duration, val serverConnectionTtl: Duration, val clientConnectionTtl: Duration) { + companion object { + /** + * Some sensible defaults, aligned with defaults of Artemis + */ + @Suppress("MagicNumber") + fun default() = TimeoutConfig(30.seconds, 60.seconds, 30.seconds) + } + } + /** A registration to handle messages of different types */ data class HandlerRegistration(val topic: String, val callback: Any) : MessageHandlerRegistration @@ -167,15 +208,21 @@ class P2PMessagingClient(val config: NodeConfiguration, // TODO Add broker CN to config for host verification in case the embedded broker isn't used val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions, threadPoolName = threadPoolName) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { - connectionTTL = 60000 - clientFailureCheckPeriod = 30000 + callTimeout = timeoutConfig.callTimeout.toMillis() + connectionTTL = timeoutConfig.serverConnectionTtl.toMillis() + clientFailureCheckPeriod = timeoutConfig.clientConnectionTtl.toMillis() minLargeMessageSize = maxMessageSize + JOURNAL_HEADER_SIZE isUseGlobalPools = nodeSerializationEnv != null } - sessionFactory = locator!!.createSessionFactory().addFailoverListener(::failoverCallback) + + sessionFactory = if (terminateOnConnectionError) { + locator!!.createSessionFactory().addFailoverListener(::failoverCallback) + } else { + locator!!.createSessionFactory() + } // Login using the node username. The broker will authenticate us as its node (as opposed to another peer) // using our TLS certificate. - // Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer + // Note that the acknowledgement of messages is not flushed to the Artemis journal until the default buffer // size of 1MB is acknowledged. val createNewSession = { sessionFactory!!.createSession(ArtemisMessagingComponent.NODE_P2P_USER, ArtemisMessagingComponent.NODE_P2P_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) } @@ -233,7 +280,8 @@ class P2PMessagingClient(val config: NodeConfiguration, private fun InnerState.registerBridgeControl(session: ClientSession, inboxes: List) { val bridgeNotifyQueue = "$BRIDGE_NOTIFY.${myIdentity.toStringShort()}" if (!session.queueQuery(SimpleString(bridgeNotifyQueue)).isExists) { - session.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue) + session.createQueue(QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) + .setTemporary(true).setDurable(false)) } val bridgeConsumer = session.createConsumer(bridgeNotifyQueue) bridgeNotifyConsumer = bridgeConsumer @@ -265,8 +313,8 @@ class P2PMessagingClient(val config: NodeConfiguration, log.info("Updating bridges on network map change: ${change::class.simpleName} ${change.node}") fun gatherAddresses(node: NodeInfo): Sequence { return state.locked { - node.legalIdentitiesAndCerts.map { - val messagingAddress = NodeAddress(it.party.owningKey) + node.legalIdentitiesAndCerts.map { partyAndCertificate -> + val messagingAddress = NodeAddress(partyAndCertificate.party.owningKey) BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false) }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() } @@ -404,12 +452,15 @@ class P2PMessagingClient(val config: NodeConfiguration, internal fun deliver(artemisMessage: ClientMessage) { artemisToCordaMessage(artemisMessage)?.let { cordaMessage -> - if (!deduplicator.isDuplicate(cordaMessage)) { + val outcome = deduplicator.checkDuplicate(cordaMessage) + if (outcome == P2PMessageDeduplicator.Outcome.NEW) { deduplicator.signalMessageProcessStart(cordaMessage) deliver(cordaMessage, artemisMessage) - } else { - log.trace { "Discard duplicate message ${cordaMessage.uniqueMessageId} for ${cordaMessage.topic}" } + } else if (outcome == P2PMessageDeduplicator.Outcome.DUPLICATE) { + log.debug { "Acknowledge duplicate message id: ${cordaMessage.uniqueMessageId} senderUUID: ${cordaMessage.senderUUID} senderSeqNo: ${cordaMessage.senderSeqNo} isSessionInit: ${cordaMessage.isSessionInit}" } messagingExecutor!!.acknowledge(artemisMessage) + } else { + log.debug { "Discard in-flight message id: ${cordaMessage.uniqueMessageId} senderUUID: ${cordaMessage.senderUUID} senderSeqNo: ${cordaMessage.senderSeqNo} isSessionInit: ${cordaMessage.isSessionInit}" } } } } @@ -463,8 +514,8 @@ class P2PMessagingClient(val config: NodeConfiguration, running = false stateHelper.active = false networkChangeSubscription?.unsubscribe() - require(p2pConsumer != null, { "stop can't be called twice" }) - require(producer != null, { "stop can't be called twice" }) + require(p2pConsumer != null) { "stop can't be called twice" } + require(producer != null) { "stop can't be called twice" } close(p2pConsumer) p2pConsumer = null @@ -524,7 +575,7 @@ class P2PMessagingClient(val config: NodeConfiguration, // If we are sending to ourselves then route the message directly to our P2P queue. RemoteInboxAddress(myIdentity).queueName } else { - // Otherwise we send the message to an internal queue for the target residing on our broker. It's then the + // Otherwise, we send the message to an internal queue for the target residing on our broker. It's then the // broker's job to route the message to the target's P2P queue. val internalTargetQueue = (address as? ArtemisAddress)?.queueName ?: throw IllegalArgumentException("Not an Artemis address") @@ -556,9 +607,13 @@ class P2PMessagingClient(val config: NodeConfiguration, val queueQuery = session.queueQuery(SimpleString(queueName)) if (!queueQuery.isExists) { log.info("Create fresh queue $queueName bound on same address") - session.createQueue(queueName, RoutingType.ANYCAST, queueName, null, true, false, - ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), - ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), exclusive, null) + session.createQueue(QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName) + .setDurable(true).setAutoCreated(false) + .setMaxConsumers(ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers()) + .setPurgeOnNoConsumers(ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers()) + .setExclusive(exclusive) + .setLastValue(null) + ) sendBridgeCreateMessage() } } @@ -567,7 +622,7 @@ class P2PMessagingClient(val config: NodeConfiguration, } override fun addMessageHandler(topic: String, callback: MessageHandler): MessageHandlerRegistration { - require(!topic.isBlank()) { "Topic must not be blank, as the empty topic is a special case." } + require(topic.isNotBlank()) { "Topic must not be blank, as the empty topic is a special case." } handlers.compute(topic) { _, handler -> if (handler != null) { throw IllegalStateException("Cannot add another acking handler for $topic, there is already an acking one") 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 a186241f64..2749f83c5d 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 @@ -17,6 +17,7 @@ import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize +import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -34,6 +35,9 @@ import java.security.PublicKey import java.security.cert.CertPathValidatorException import java.util.* import javax.annotation.concurrent.ThreadSafe +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id import javax.persistence.PersistenceException /** Database-based network map cache. */ @@ -61,6 +65,18 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, @Volatile private lateinit var rotatedNotaries: Set + @Entity + @javax.persistence.Table(name = "node_named_identities") + data class PersistentPartyToPublicKeyHash( + @Id + @Suppress("MagicNumber") // database column width + @Column(name = "name", length = 128, nullable = false) + var name: String = "", + + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = true) + var publicKeyHash: String? = "" + ) + // Notary whitelist may contain multiple identities with the same X.500 name after certificate rotation. // Exclude duplicated entries, which are not present in the network map. override val notaryIdentities: List get() = notaries.map { it.identity } @@ -294,6 +310,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, synchronized(_changed) { database.transaction { removeInfoDB(session, node) + archiveNamedIdentity(node) changePublisher.onNext(MapChange.Removed(node)) } } @@ -302,6 +319,12 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, logger.debug { "Done removing node with info: $node" } } + private fun archiveNamedIdentity(nodeInfo: NodeInfo) { + nodeInfo.legalIdentities.forEach { party -> + identityService.archiveNamedIdentity(party.name.toString(), party.owningKey.toStringShort()) + } + } + override val allNodes: List get() { return database.transaction { @@ -428,7 +451,10 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, database.transaction { val result = getAllNodeInfos(session) logger.debug { "Number of node infos to be cleared: ${result.size}" } - for (nodeInfo in result) session.remove(nodeInfo) + for (nodeInfo in result) { + session.remove(nodeInfo) + archiveNamedIdentity(nodeInfo.toNodeInfo()) + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index aeeea1dba8..24046f2941 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -101,7 +101,9 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: // Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here, // as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add // to the memory pressure at all here. - private const val transactionSignatureOverheadEstimate = 1024 + private const val TRANSACTION_SIGNATURE_OVERHEAD_BYTES = 1024 + private const val TXCACHEVALUE_OVERHEAD_BYTES = 80 + private const val SECUREHASH_OVERHEAD_BYTES = 24 private val logger = contextLogger() @@ -134,13 +136,13 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: ) }, persistentEntityClass = DBTransaction::class.java, - weighingFunc = { hash, tx -> hash.size + weighTx(tx) } + weighingFunc = { hash, tx -> SECUREHASH_OVERHEAD_BYTES + hash.size + weighTx(tx) } ) } - private fun weighTx(tx: AppendOnlyPersistentMapBase.Transactional): Int { - val actTx = tx.peekableValue ?: return 0 - return actTx.sigs.sumBy { it.size + transactionSignatureOverheadEstimate } + actTx.txBits.size + private fun weighTx(actTx: TxCacheValue?): Int { + if (actTx == null) return 0 + return TXCACHEVALUE_OVERHEAD_BYTES + actTx.sigs.sumBy { it.size + TRANSACTION_SIGNATURE_OVERHEAD_BYTES } + actTx.txBits.size } private val log = contextLogger() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index e6aee94e7f..6ea173ef4d 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -88,6 +88,9 @@ class NodeAttachmentService @JvmOverloads constructor( while (true) { val cursor = jar.nextJarEntry ?: break + // Security check to stop directory traversal from filename entry + require(!(cursor.name.contains("../"))) { "Bad character in ${cursor.name}" } + require(!(cursor.name.contains("..\\"))) { "Bad character in ${cursor.name}" } if (manifestHasEntries && !allManifestEntries!!.remove(cursor.name)) extraFilesNotFoundInEntries.add(cursor) val entryPath = Paths.get(cursor.name) // Security check to stop zips trying to escape their rightful place. diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt index 818923697d..6ae79d378e 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt @@ -8,6 +8,7 @@ import net.corda.node.internal.artemis.* import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECURITY_CONFIG import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.utilities.artemis.startSynchronously import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -51,20 +52,19 @@ class ArtemisRpcBroker internal constructor( } } + @Suppress("TooGenericExceptionCaught") override fun start() { logger.debug { "Artemis RPC broker is starting for: $addresses" } try { - server.start() - } catch (e: IOException) { + server.startSynchronously() + } catch (e: Throwable) { logger.error("Unable to start message broker", e) if (e.isBindingError()) { throw AddressBindingException(adminAddressOptional?.let { setOf(it, addresses.primary) } ?: setOf(addresses.primary)) } else { + logger.error("Unexpected error starting message broker", e) throw e } - } catch (th: Throwable) { - logger.error("Unexpected error starting message broker", th) - throw th } logger.debug("Artemis RPC broker is started.") } @@ -90,7 +90,6 @@ class ArtemisRpcBroker internal constructor( val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener) return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply { - registerActivationFailureListener { exception -> throw exception } registerPostQueueDeletionCallback { address, qName -> logger.debug("Queue deleted: $qName for $address") } } } diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index 11ecd7e2c1..13af138c8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -12,8 +12,8 @@ import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcAcceptorTcpTr import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.SimpleString -import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings @@ -37,14 +37,14 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, } acceptorConfigurations = acceptorConfigurationsSet - queueConfigurations = queueConfigurations() + queueConfigs = queueConfigurations() managementNotificationAddress = SimpleString(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS) addressesSettings = mapOf( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply { maxSizeBytes = 5L * maxMessageSize addressFullMessagePolicy = AddressFullMessagePolicy.PAGE - pageSizeBytes = 1L * maxMessageSize + pageSizeBytes = maxMessageSize } ) @@ -76,7 +76,11 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(BrokerJaasLoginModule.RPC_ROLE, send = true)) securitySettingPlugins.add(rolesAdderOnLogin) - securityInvalidationInterval = ArtemisMessagingComponent.SECURITY_INVALIDATION_INTERVAL + + // Effectively disable security cache as permissions might change dynamically when e.g. DB is updated + authenticationCacheSize = 0 + authorizationCacheSize = 0 + securityInvalidationInterval = 0 } private fun enableJmx() { @@ -85,19 +89,19 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, } private fun initialiseSettings(maxMessageSize: Int, journalBufferTimeout: Int?) { - // Enable built in message deduplication. Note we still have to do our own as the delayed commits - // and our own definition of commit mean that the built in deduplication cannot remove all duplicates. + // Enable built-in message deduplication. Note we still have to do our own as the delayed commits + // and our own definition of commit means that the built-in deduplication cannot remove all the duplicates. idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess isPersistIDCache = true isPopulateValidatedUser = true - journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. + journalBufferSize_NIO = maxMessageSize // Artemis default is 490 KB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. journalBufferTimeout_NIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutNio() journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store. journalBufferTimeout_AIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutAio() - journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB. + journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10 MB. } - private fun queueConfigurations(): List { + private fun queueConfigurations(): List { return listOf( queueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME, durable = false), queueConfiguration( @@ -122,15 +126,8 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, pagingDirectory = (baseDirectory / "paging").toString() } - private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration { - val configuration = CoreQueueConfiguration() - - configuration.name = name - configuration.address = address - configuration.filterString = filter - configuration.isDurable = durable - - return configuration + private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): QueueConfiguration { + return QueueConfiguration(name).setAddress(address).setFilterString(filter).setDurable(durable) } private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false, diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 883da29e92..f62c40ee1c 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -14,6 +14,7 @@ import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.messaging.P2PMessageDeduplicator +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService @@ -49,7 +50,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentHashToPublicKey::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java, - PublicKeyHashToExternalId::class.java + PublicKeyHashToExternalId::class.java, + PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java )) { override val migrationResource = "node-core.changelog-master" } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index 519b2bd3d5..2d314e9c3b 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party import net.corda.core.internal.DeclaredField +import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.ThreadBox import net.corda.core.internal.TimedFlow import net.corda.core.internal.VisibleForTesting @@ -21,6 +22,7 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.FinalityHandler +import net.corda.node.services.statemachine.transitions.StartedFlowTransition import org.hibernate.exception.ConstraintViolationException import rx.subjects.PublishSubject import java.io.Closeable @@ -29,10 +31,9 @@ import java.sql.SQLTransientConnectionException import java.time.Clock import java.time.Duration import java.time.Instant -import java.util.* +import java.util.Timer import java.util.concurrent.ConcurrentHashMap import javax.persistence.PersistenceException -import kotlin.collections.HashMap import kotlin.concurrent.timerTask import kotlin.math.pow @@ -103,17 +104,6 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, * Flows should be removed from [flowsInHospital] when they have completed a successful transition. */ private val flowsInHospital = ConcurrentHashMap() - - /** - * Returns true if the flow is currently being treated in the hospital. - * The differs to flows with a medical history (which can accessed via [StaffedFlowHospital.contains]). - */ - @VisibleForTesting - internal fun flowInHospital(runId: StateMachineRunId): Boolean { - // The .keys avoids https://youtrack.jetbrains.com/issue/KT-18053 - return runId in flowsInHospital.keys - } - private val mutex = ThreadBox(object { /** * Contains medical history of every flow (a patient) that has entered the hospital. A flow can leave the hospital, @@ -347,7 +337,7 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } } - operator fun contains(flowId: StateMachineRunId) = mutex.locked { flowId in flowPatients } + operator fun contains(flowId: StateMachineRunId) = flowId in flowsInHospital.keys override fun close() { hospitalJobTimer.cancel() @@ -485,13 +475,22 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, "the flow by re-starting the node. State machine state: $currentState", newError) Diagnosis.OVERNIGHT_OBSERVATION } else if (isFromReceiveFinalityFlow(newError)) { - if (isErrorPropagatedFromCounterparty(newError) && isErrorThrownDuringReceiveFinality(newError)) { - // no need to keep around the flow, since notarisation has already failed at the counterparty. - Diagnosis.NOT_MY_SPECIALTY - } else { - log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " + - "the flow by re-starting the node. State machine state: $currentState", newError) - Diagnosis.OVERNIGHT_OBSERVATION + when { + isErrorPropagatedFromCounterparty(newError) && isErrorThrownDuringReceiveTransactionFlow(newError) -> { + // no need to keep around the flow, since notarisation has already failed at the counterparty. + Diagnosis.NOT_MY_SPECIALTY + } + isEndSessionErrorThrownDuringReceiveTransactionFlow(newError) -> { + // Typically occurs if the initiating flow catches a notary exception and ends their flow successfully. + Diagnosis.NOT_MY_SPECIALTY + } + else -> { + log.warn( + "Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " + + "the flow by re-starting the node. State machine state: $currentState", newError + ) + Diagnosis.OVERNIGHT_OBSERVATION + } } } else { Diagnosis.NOT_MY_SPECIALTY @@ -523,13 +522,26 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, * This is because in the latter case, the transaction might have already been finalised and deleting the flow * would introduce risk for inconsistency between nodes. */ - private fun isErrorThrownDuringReceiveFinality(error: Throwable): Boolean { + private fun isErrorThrownDuringReceiveTransactionFlow(error: Throwable): Boolean { val strippedStacktrace = error.stackTrace .filterNot { it?.className?.contains("counter-flow exception from peer") ?: false } .filterNot { it?.className?.startsWith("net.corda.node.services.statemachine.") ?: false } return strippedStacktrace.isNotEmpty() && strippedStacktrace.first().className.startsWith(ReceiveTransactionFlow::class.qualifiedName!!) } + + /** + * Checks if an end session error exception was thrown and that it did so within [ReceiveTransactionFlow]. + * + * The check for [ReceiveTransactionFlow] is important to ensure that the session didn't end within [ResolveTransactionsFlow] which + * implies that it has been receiving the transaction's dependencies and therefore ending before receiving the whole transaction + * is incorrect behaviour. + */ + private fun isEndSessionErrorThrownDuringReceiveTransactionFlow(error: Throwable): Boolean { + return error is UnexpectedFlowEndException + && error.message?.contains(StartedFlowTransition.UNEXPECTED_SESSION_END_MESSAGE) == true + && isErrorThrownDuringReceiveTransactionFlow(error) + } } /** 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 2f2b8a167b..79bad802fe 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 @@ -28,6 +28,7 @@ class StartedFlowTransition( companion object { private val logger: Logger = contextLogger() + const val UNEXPECTED_SESSION_END_MESSAGE = "Received session end message instead of a data session message. Mismatched send and receive?" } override fun transition(): TransitionResult { @@ -253,7 +254,7 @@ class StartedFlowTransition( newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toArrayList()) // at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message. resultMessages[sessionId] = if (messages[0] is EndSessionMessage) { - throw UnexpectedFlowEndException("Received session end message instead of a data session message. Mismatched send and receive?") + throw UnexpectedFlowEndException(UNEXPECTED_SESSION_END_MESSAGE) } else { (messages[0] as DataSessionMessage).payload } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 3e8387803f..b5f9b327c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -272,7 +272,7 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: class HibernateQueryCriteriaParser(val contractStateType: Class, val contractStateTypeMappings: Map>, override val criteriaBuilder: CriteriaBuilder, - val criteriaQuery: CriteriaQuery, + val criteriaQuery: CriteriaQuery<*>, val vaultStates: Root) : AbstractQueryCriteriaParser(), IQueryCriteriaParser { private companion object { private val log = contextLogger() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 9c90c36dd3..2f5fbd43a6 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -35,7 +35,6 @@ import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.SortAttribute -import net.corda.core.node.services.vault.builder import net.corda.core.observable.internal.OnResilientSubscribe import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken @@ -69,17 +68,21 @@ import java.security.PublicKey import java.sql.SQLException import java.time.Clock import java.time.Instant -import java.util.Arrays -import java.util.UUID +import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet import java.util.stream.Stream import javax.persistence.PersistenceException import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.CriteriaQuery import javax.persistence.criteria.CriteriaUpdate import javax.persistence.criteria.Predicate import javax.persistence.criteria.Root +import kotlin.collections.ArrayList +import kotlin.collections.LinkedHashSet +import kotlin.collections.component1 +import kotlin.collections.component2 /** * The vault service handles storage, retrieval and querying of states. @@ -706,7 +709,8 @@ class NodeVaultService( paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { - val (query, stateTypes) = createQuery(criteria, contractStateType, sorting) + val (criteriaQuery, criteriaParser) = buildCriteriaQuery(criteria, contractStateType, sorting) + val query = getSession().createQuery(criteriaQuery) query.setResultWindow(paging) val statesMetadata: MutableList = mutableListOf() @@ -736,7 +740,7 @@ class NodeVaultService( else -> queryTotalStateCount(criteria, contractStateType) } - return Vault.Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults) + return Vault.Page(states, statesMetadata, totalStatesAvailable, criteriaParser.stateTypes, otherResults) } private fun Query.resultStream(paging: PageSpecification): Stream { @@ -765,19 +769,17 @@ class NodeVaultService( } } - private fun queryTotalStateCount(baseCriteria: QueryCriteria, contractStateType: Class): Long { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val criteria = baseCriteria.and(countCriteria) - val (query) = createQuery(criteria, contractStateType, null) - val results = query.resultList - return results.last().toArray().last() as Long + private fun queryTotalStateCount(criteria: QueryCriteria, contractStateType: Class): Long { + val (criteriaQuery, criteriaParser) = buildCriteriaQuery(criteria, contractStateType, null) + criteriaQuery.select(criteriaBuilder.count(criteriaParser.vaultStates)) + val query = getSession().createQuery(criteriaQuery) + return query.singleResult } - private fun createQuery(criteria: QueryCriteria, - contractStateType: Class, - sorting: Sort?): Pair, Vault.StateStatus> { - val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + private inline fun buildCriteriaQuery(criteria: QueryCriteria, + contractStateType: Class, + sorting: Sort?): Pair, HibernateQueryCriteriaParser> { + val criteriaQuery = criteriaBuilder.createQuery(T::class.java) val criteriaParser = HibernateQueryCriteriaParser( contractStateType, contractStateTypeMappings, @@ -786,8 +788,7 @@ class NodeVaultService( criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) ) criteriaParser.parse(criteria, sorting) - val query = getSession().createQuery(criteriaQuery) - return Pair(query, criteriaParser.stateTypes) + return Pair(criteriaQuery, criteriaParser) } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index f45ddbb7cf..570172fa06 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -32,8 +32,10 @@ abstract class AppendOnlyPersistentMapBase( private val log = contextLogger() } + protected class PendingKeyValue(val transactions: MutableSet, val estimatedSize: Int) + protected abstract val cache: LoadingCache> - protected val pendingKeys = ConcurrentHashMap>() + protected val pendingKeys = ConcurrentHashMap() /** * Returns the value associated with the key, first loading that value from the storage if necessary. @@ -85,7 +87,8 @@ abstract class AppendOnlyPersistentMapBase( // for cases where the value passed to set differs from that in the cache, but an update function has decided that this // differing value should not be written to the database. if (wasWritten) { - Transactional.InFlight(this, key, _readerValueLoader = { loadValue(key) }).apply { alsoWrite(value) } + Transactional.InFlight(this, key, weight = weight(key, value), _readerValueLoader = { loadValue(key) }) + .apply { alsoWrite(value) } } else { oldValueInCache } @@ -120,7 +123,8 @@ abstract class AppendOnlyPersistentMapBase( Transactional.Committed(oldValue) } else { // Some database transactions, including us, writing, with readers seeing whatever is in the database and writers seeing the (in memory) value. - Transactional.InFlight(this, key, _readerValueLoader = { loadValue(key) }).apply { alsoWrite(value) } + Transactional.InFlight(this, key, weight = weight(key, value), _readerValueLoader = { loadValue(key) }) + .apply { alsoWrite(value) } } } @@ -214,11 +218,12 @@ abstract class AppendOnlyPersistentMapBase( protected fun transactionalLoadValue(key: K): Transactional { // This gets called if a value is read and the cache has no Transactional for this key yet. - return if (anyoneWriting(key)) { + val estimatedSize = anyoneWriting(key) + return if (estimatedSize != -1) { // If someone is writing (but not us) // For those not writing, they need to re-load the value from the database (which their database transaction MIGHT see). // For those writing, they need to re-load the value from the database (which their database transaction CAN see). - Transactional.InFlight(this, key, { loadValue(key) }, { loadValue(key)!! }) + Transactional.InFlight(this, key, estimatedSize, { loadValue(key) }, { loadValue(key)!! }) } else { // If no one is writing, then the value may or may not exist in the database. Transactional.Unknown(this, key) { loadValue(key) } @@ -240,21 +245,24 @@ abstract class AppendOnlyPersistentMapBase( } // Helpers to know if transaction(s) are currently writing the given key. - private fun weAreWriting(key: K): Boolean = pendingKeys[key]?.contains(contextTransaction) ?: false + private fun weAreWriting(key: K): Boolean = pendingKeys[key]?.transactions?.contains(contextTransaction) ?: false - private fun anyoneWriting(key: K): Boolean = pendingKeys[key]?.isNotEmpty() ?: false + private fun anyoneWriting(key: K): Int = pendingKeys[key]?.estimatedSize ?: -1 + + protected open fun weight(key: K, value: V): Int = 1 // Indicate this database transaction is a writer of this key. - private fun addPendingKey(key: K, databaseTransaction: DatabaseTransaction): Boolean { + private fun addPendingKey(key: K, databaseTransaction: DatabaseTransaction, estimatedSize: Int): Boolean { var added = true - pendingKeys.compute(key) { _, oldSet -> + pendingKeys.compute(key) { _, value: PendingKeyValue? -> + val oldSet = value?.transactions if (oldSet == null) { val newSet = HashSet(0) newSet += databaseTransaction - newSet + PendingKeyValue(newSet, estimatedSize) } else { added = oldSet.add(databaseTransaction) - oldSet + value } } return added @@ -262,12 +270,13 @@ abstract class AppendOnlyPersistentMapBase( // Remove this database transaction as a writer of this key, because the transaction committed or rolled back. private fun removePendingKey(key: K, databaseTransaction: DatabaseTransaction) { - pendingKeys.compute(key) { _, oldSet -> + pendingKeys.compute(key) { _, value: PendingKeyValue? -> + val oldSet = value?.transactions if (oldSet == null) { - oldSet + null } else { oldSet -= databaseTransaction - if (oldSet.size == 0) null else oldSet + if (oldSet.size == 0) null else value } } } @@ -278,10 +287,12 @@ abstract class AppendOnlyPersistentMapBase( * There are 3 states. Globally missing, globally visible, and being written in a transaction somewhere now or in * the past (and it rolled back). */ + @Suppress("MagicNumber") sealed class Transactional { abstract val value: T abstract val isPresent: Boolean abstract val peekableValue: T? + abstract val shallowSize: Int fun orElse(alt: T?) = if (isPresent) value else alt @@ -291,6 +302,8 @@ abstract class AppendOnlyPersistentMapBase( get() = true override val peekableValue: T? get() = value + override val shallowSize: Int + get() = 48 } // No one can see it. @@ -301,6 +314,8 @@ abstract class AppendOnlyPersistentMapBase( get() = false override val peekableValue: T? get() = null + override val shallowSize: Int + get() = 16 } // No one is writing, but we haven't looked in the database yet. This can only be when there are no writers. @@ -323,12 +338,15 @@ abstract class AppendOnlyPersistentMapBase( } val isResolved: Boolean get() = valueWithoutIsolationDelegate.isInitialized() override val peekableValue: T? get() = if (isResolved && isPresent) value else null + override val shallowSize: Int + get() = 128 } // Written in a transaction (uncommitted) somewhere, but there's a small window when this might be seen after commit, // hence the committed flag. class InFlight(private val map: AppendOnlyPersistentMapBase, private val key: K, + val weight: Int, private val _readerValueLoader: () -> T?, private val _writerValueLoader: () -> T = { throw IllegalAccessException("No value loader provided") }) : Transactional() { @@ -352,7 +370,7 @@ abstract class AppendOnlyPersistentMapBase( val tx = contextTransaction val strongKey = key val strongMap = map - if (map.addPendingKey(key, tx)) { + if (map.addPendingKey(key, tx, weight)) { // If the transaction commits, update cache to make globally visible if we're first for this key, // and then stop saying the transaction is writing the key. tx.onCommit { @@ -414,6 +432,9 @@ abstract class AppendOnlyPersistentMapBase( // The value from the perspective of the eviction algorithm of the cache. i.e. we want to reveal memory footprint to it etc. override val peekableValue: T? get() = if (writerValueLoader.get() != _writerValueLoader) writerValueLoader.get()() else if (readerValueLoader.get() != _readerValueLoader) readerValueLoader.get()() else null + + override val shallowSize: Int + get() = 256 } } } @@ -445,15 +466,24 @@ class WeightBasedAppendOnlyPersistentMap( fromPersistentEntity: (E) -> Pair, toPersistentEntity: (key: K, value: V) -> E, persistentEntityClass: Class, - weighingFunc: (K, Transactional) -> Int + private val weighingFunc: (K, V?) -> Int ) : AppendOnlyPersistentMapBase( toPersistentEntityKey, fromPersistentEntity, toPersistentEntity, persistentEntityClass) { + + override fun weight(key: K, value: V): Int = weighingFunc(key, value) + override val cache = NonInvalidatingWeightBasedCache( cacheFactory = cacheFactory, name = name, - weigher = Weigher { key, value -> weighingFunc(key, value) }, + weigher = Weigher { key, value: Transactional -> + value.shallowSize + if (value is Transactional.InFlight<*, *>) { + value.weight * 2 + } else { + weighingFunc(key, value.peekableValue) + } + }, loadFunction = { key: K -> transactionalLoadValue(key) }) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt index 9f9698164f..4121a92cb9 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt @@ -73,7 +73,7 @@ class InfrequentlyMutatedCache(name: String, cacheFactory: Nam backingCache.invalidateAll() } - private fun invalidate(key: K, value: Wrapper.Invalidated): Wrapper.Invalidated { + private fun invalidate(key: K, value: Wrapper.Invalidated): Wrapper.Invalidated? { val tx = contextTransactionOrNull value.invalidators.incrementAndGet() currentlyInvalid[key] = value @@ -81,7 +81,10 @@ class InfrequentlyMutatedCache(name: String, cacheFactory: Nam // When we close, we can't start using caching again until all simultaneously open transactions are closed. tx.onClose { tx.database.onAllOpenTransactionsClosed { decrementInvalidators(key, value) } } } else { - decrementInvalidators(key, value) + if (value.invalidators.decrementAndGet() == 0) { + currentlyInvalid.remove(key) + return null + } } return value } diff --git a/node/src/main/kotlin/net/corda/node/utilities/artemis/ArtemisStartupUtil.kt b/node/src/main/kotlin/net/corda/node/utilities/artemis/ArtemisStartupUtil.kt new file mode 100644 index 0000000000..1f8675255a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/artemis/ArtemisStartupUtil.kt @@ -0,0 +1,22 @@ +package net.corda.node.utilities.artemis + +import net.corda.core.utilities.getOrThrow +import org.apache.activemq.artemis.core.server.ActivateCallback +import org.apache.activemq.artemis.core.server.ActiveMQServer +import java.util.concurrent.CompletableFuture + +fun ActiveMQServer.startSynchronously() { + val startupFuture = CompletableFuture() + registerActivateCallback(object: ActivateCallback { + override fun activationComplete() { + startupFuture.complete(Unit) + } + }) + registerActivationFailureListener { + startupFuture.completeExceptionally(it) + } + + start() + + startupFuture.getOrThrow() +} \ No newline at end of file diff --git a/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.OpenSSLContextFactory b/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.OpenSSLContextFactory new file mode 100644 index 0000000000..6b69d9d3ff --- /dev/null +++ b/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.OpenSSLContextFactory @@ -0,0 +1 @@ +net.corda.node.services.messaging.NodeOpenSSLContextFactory diff --git a/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory b/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory new file mode 100644 index 0000000000..59e57dca26 --- /dev/null +++ b/node/src/main/resources/META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory @@ -0,0 +1 @@ +net.corda.node.services.messaging.NodeSSLContextFactory diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index 9a682dc909..fe333ea9df 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -27,6 +27,7 @@ + diff --git a/node/src/main/resources/migration/node-core.changelog-v22.xml b/node/src/main/resources/migration/node-core.changelog-v22.xml new file mode 100644 index 0000000000..b7d454c2cc --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v22.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt b/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt index 3f316258e7..b6b9288884 100644 --- a/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/artemis/UserValidationPluginTest.kt @@ -1,24 +1,34 @@ package net.corda.node.internal.artemis +import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.doThrow import com.nhaarman.mockito_kotlin.whenever import net.corda.coretesting.internal.rigorousMock import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.core.client.impl.ClientMessageImpl import org.apache.activemq.artemis.core.server.ServerSession import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage -import org.apache.activemq.artemis.protocol.amqp.converter.AMQPConverter +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPStandardMessage import org.assertj.core.api.Assertions import org.junit.Test class UserValidationPluginTest { private val plugin = UserValidationPlugin() - private val coreMessage = ClientMessageImpl(0, false, 0, System.currentTimeMillis(), 4.toByte(), 1024) - private val amqpMessage get() = AMQPConverter.getInstance().fromCore(coreMessage) + private val coreMessage = ClientMessageImpl(0, false, 0, System.currentTimeMillis(), + 4.toByte(), 1024) + private val amqpMessage: AMQPMessage + get() { + return rigorousMock().also { + doReturn(coreMessage.validatedUserID).whenever(it).getStringProperty(Message.HDR_VALIDATED_USER) + } + } + private val session = rigorousMock().also { doReturn(ArtemisMessagingComponent.PEER_USER).whenever(it).username doReturn(ALICE_NAME.toString()).whenever(it).validatedUser @@ -31,16 +41,17 @@ class UserValidationPluginTest { @Test(timeout = 300_000) fun `accept AMQP message with user`() { - coreMessage.putStringProperty("_AMQ_VALIDATED_USER", ALICE_NAME.toString()) + coreMessage.validatedUserID = ALICE_NAME.toString() plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) } @Test(timeout = 300_000) fun `reject AMQP message with different user`() { - coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + coreMessage.validatedUserID = BOB_NAME.toString() + val localAmqpMessage = amqpMessage Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { - plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) - }.withMessageContaining("_AMQ_VALIDATED_USER") + plugin.beforeSend(session, rigorousMock(), localAmqpMessage, direct = false, noAutoCreateQueue = false) + }.withMessageContaining(Message.HDR_VALIDATED_USER.toString()) } @Test(timeout = 300_000) @@ -49,7 +60,7 @@ class UserValidationPluginTest { doReturn(ArtemisMessagingComponent.NODE_P2P_USER).whenever(it).username doReturn(ALICE_NAME.toString()).whenever(it).validatedUser } - coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) + coreMessage.validatedUserID = BOB_NAME.toString() plugin.beforeSend(internalSession, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) } @@ -62,11 +73,8 @@ class UserValidationPluginTest { @Test(timeout = 300_000) fun `reject message with exception`() { - coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) - val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { - override fun getStringProperty(key: SimpleString?): String { - throw IllegalStateException("My exception") - } + val messageWithException = rigorousMock().also { + doThrow(IllegalStateException("My exception")).whenever(it).getStringProperty(any()) } // Artemis swallows all exceptions except ActiveMQException, so making sure that proper exception is thrown Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { @@ -76,9 +84,8 @@ class UserValidationPluginTest { @Test(timeout = 300_000) fun `reject message with security exception`() { - coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) - val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { - override fun getStringProperty(key: SimpleString?): String { + val messageWithException = object : AMQPStandardMessage(0, ByteArray(0), null) { + override fun getApplicationPropertiesMap(createIfAbsent: Boolean): MutableMap { throw ActiveMQSecurityException("My security exception") } } diff --git a/node/src/test/kotlin/net/corda/node/migration/MigrationTestSchema.kt b/node/src/test/kotlin/net/corda/node/migration/MigrationTestSchema.kt index e9d76e030a..2ee02e3530 100644 --- a/node/src/test/kotlin/net/corda/node/migration/MigrationTestSchema.kt +++ b/node/src/test/kotlin/net/corda/node/migration/MigrationTestSchema.kt @@ -50,7 +50,7 @@ object IdentityTestSchemaV1 : MappedSchema( @Column(name = "name", length = 128, nullable = false) var name: String = "", - @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false) + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = true) var publicKeyHash: String = "" ) diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 781a980c7b..273e898063 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -10,10 +10,10 @@ import net.corda.core.internal.div import net.corda.core.internal.toPath import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds +import net.corda.node.services.config.shell.SSHDConfiguration import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive import net.corda.testing.core.ALICE_NAME import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.tools.shell.SSHDConfiguration import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertEquals 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 497646a679..5c87059305 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 @@ -1,16 +1,22 @@ package net.corda.node.services.network import net.corda.core.crypto.toStringShort +import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.NetworkMapCache import net.corda.core.serialization.serialize import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.TestStartedNode import org.assertj.core.api.Assertions.assertThat import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import java.math.BigInteger import kotlin.test.assertEquals @@ -18,6 +24,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class NetworkMapCacheTest { + private val TestStartedNode.party get() = info.legalIdentities.first() private val mockNet = InternalMockNetwork() @After @@ -25,6 +32,153 @@ class NetworkMapCacheTest { mockNet.stopNodes() } + @Test(timeout=300_000) + fun `unknown Party object gets recorded as null entry in node_named_identities table`() { + val bobNode = mockNet.createPartyNode(BOB_NAME) + assertEquals(null, bobNode.services.identityService.wellKnownPartyFromX500Name(CHARLIE_NAME)) + bobNode.database.transaction { + val cb = session.criteriaBuilder + val query = cb.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val root = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + + val matchPublicKey = cb.isNull(root.get("publicKeyHash")) + val matchName = cb.equal(root.get("name"), CHARLIE_NAME.toString()) + query.select(root).where(cb.and(matchName, matchPublicKey)) + + val resultList = session.createQuery(query).resultList + assertEquals(1, resultList.size) + } + } + + @Test(timeout=300_000) + fun `check Party object can still be retrieved when not in node_named_identities table`() { + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bobCache: NetworkMapCache = bobNode.services.networkMapCache + + val bobCacheInternal = bobCache as NetworkMapCacheInternal + assertNotNull(bobCacheInternal) + bobCache.removeNode(aliceNode.info) + + val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort() + + // Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry. + removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash) + assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size) + } + + private fun removeNodeFromNodeNamedIdentitiesTable(node: TestStartedNode, publicKeyHashToRemove: String) { + // Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry. + node.database.transaction { + val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val queryRoot = deleteQuery.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + deleteQuery.where(session.criteriaBuilder.equal(queryRoot.get("publicKeyHash"), publicKeyHashToRemove)) + session.createQuery(deleteQuery).executeUpdate() + } + } + + private fun queryNodeNamedIdentities(node: TestStartedNode, party: CordaX500Name, publicKeyHash: String): List { + return node.database.transaction { + val cb = session.criteriaBuilder + val query = cb.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val root = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java) + val matchPublicKeyHash = cb.equal(root.get("publicKeyHash"), publicKeyHash) + val matchName = cb.equal(root.get("name"), party.toString()) + query.select(root).where(cb.and(matchName, matchPublicKeyHash)) + session.createQuery(query).resultList + } + } + + @Test(timeout=300_000) + fun `check removed node is inserted into node_name_identities table and then its Party object can be retrieved`() { + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bobCache: NetworkMapCache = bobNode.services.networkMapCache + + val bobCacheInternal = bobCache as NetworkMapCacheInternal + assertNotNull(bobCacheInternal) + + val aliceParty1 = bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME) + println("alicePart1 = $aliceParty1") + bobCache.removeNode(aliceNode.info) + + val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort() + assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size) + assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + } + + @Test(timeout=300_000) + fun `check two removed nodes are both archived and then both Party objects are retrievable`() { + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) + val bobCache: NetworkMapCache = bobNode.services.networkMapCache + + val bobCacheInternal = bobCache as NetworkMapCacheInternal + assertNotNull(bobCacheInternal) + bobCache.removeNode(aliceNode.info) + bobCache.removeNode(charlieNode.info) + + val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort() + val charliePubKeyHash = charlieNode.info.legalIdentities[0].owningKey.toStringShort() + assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size) + assertEquals(1, queryNodeNamedIdentities(bobNode, CHARLIE_NAME, charliePubKeyHash).size) + assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + assertEquals(charlieNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(CHARLIE_NAME)) + } + + @Test(timeout=300_000) + fun `check latest identity returned according to certificate after identity mock rotatated`() { + val aliceNode1 = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bobCache: NetworkMapCache = bobNode.services.networkMapCache + val alicePubKeyHash1 = aliceNode1.info.legalIdentities[0].owningKey.toStringShort() + val bobCacheInternal = bobCache as NetworkMapCacheInternal + assertNotNull(bobCacheInternal) + bobCache.removeNode(aliceNode1.info) + // Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry. + removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash1) + val aliceNode2 = mockNet.createPartyNode(ALICE_NAME) + val alicePubKeyHash2 = aliceNode2.info.legalIdentities[0].owningKey.toStringShort() + bobCache.removeNode(aliceNode2.info) + // Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry. + removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash2) + val retrievedParty = bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME) + // For both identity certificates the valid from date is the start of the day, so either could be returned. + assertTrue(aliceNode2.party == retrievedParty || aliceNode1.party == retrievedParty) + } + + @Test(timeout=300_000) + fun `latest identity is archived after identity rotated`() { + var aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + val bobCache: NetworkMapCache = bobNode.services.networkMapCache + + val bobCacheInternal = bobCache as NetworkMapCacheInternal + assertNotNull(bobCacheInternal) + bobCache.removeNode(aliceNode.info) + + fun checkArchivedIdentity(bobNode: TestStartedNode, aliceNode: TestStartedNode) { + val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort() + bobNode.database.transaction { + val hashToIdentityStatement = database.dataSource.connection.prepareStatement("SELECT name, pk_hash FROM node_named_identities WHERE pk_hash=?") + hashToIdentityStatement.setString(1, alicePubKeyHash) + val aliceResultSet = hashToIdentityStatement.executeQuery() + + Assert.assertTrue(aliceResultSet.next()) + Assert.assertEquals(ALICE_NAME.toString(), aliceResultSet.getString("name")) + Assert.assertEquals(alicePubKeyHash.toString(), aliceResultSet.getString("pk_hash")) + Assert.assertFalse(aliceResultSet.next()) + } + } + checkArchivedIdentity(bobNode, aliceNode) + aliceNode.dispose() + aliceNode = mockNet.createPartyNode(ALICE_NAME) + bobCache.removeNode(aliceNode.info) + checkArchivedIdentity(bobNode, aliceNode) + } + @Test(timeout=300_000) fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index e419df5d01..7c52587a3f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -46,14 +46,19 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore import org.junit.Test +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.InputStream import java.net.URL import java.nio.charset.StandardCharsets import java.nio.file.FileAlreadyExistsException import java.nio.file.FileSystem import java.nio.file.Path import java.util.* +import java.util.jar.JarEntry import java.util.jar.JarInputStream +import java.util.jar.JarOutputStream +import java.util.jar.Manifest import kotlin.streams.toList import kotlin.test.* @@ -788,6 +793,32 @@ class NodeAttachmentServiceTest { } } + @Test(timeout=300_000) + fun `attachments containing jar entries whose names expose malicious directory traversal are prevented`() { + + fun createJarWithJarEntryTraversalAttack(jarEntryName: String): InputStream { + val byteArrayOutputStream = ByteArrayOutputStream() + JarOutputStream(byteArrayOutputStream, Manifest()).apply { + putNextEntry(JarEntry(jarEntryName)) + write("some-text".toByteArray()) + closeEntry() + close() + } + return ByteArrayInputStream(byteArrayOutputStream.toByteArray()) + } + + val traversalAttackJarWin = createJarWithJarEntryTraversalAttack("..\\attack") + val traversalAttackJarUnix = createJarWithJarEntryTraversalAttack("../attack") + + assertFailsWith(IllegalArgumentException::class) { + NodeAttachmentService.checkIsAValidJAR(traversalAttackJarWin) + } + + assertFailsWith(IllegalArgumentException::class) { + NodeAttachmentService.checkIsAValidJAR(traversalAttackJarUnix) + } + } + @Test(timeout=300_000) fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() { SelfCleaningDir().use { file -> diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt index d1a96ccda5..2dbd77b3ed 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryExceptionsTests.kt @@ -22,7 +22,7 @@ class VaultQueryExceptionsTests : VaultQueryParties by rule { @ClassRule @JvmField - val rule = object : VaultQueryTestRule() { + val rule = object : VaultQueryTestRule(persistentServices = false) { override val cordappPackages = listOf( "net.corda.testing.contracts", "net.corda.finance.contracts", diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index b06518667c..94a6eda019 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -4,6 +4,7 @@ import com.nhaarman.mockito_kotlin.mock import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.packageName @@ -37,6 +38,7 @@ import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndPersistentServices import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatCode @@ -102,7 +104,7 @@ interface VaultQueryParties { val cordappPackages: List } -open class VaultQueryTestRule : ExternalResource(), VaultQueryParties { +open class VaultQueryTestRule(private val persistentServices: Boolean) : ExternalResource(), VaultQueryParties { override val alice = TestIdentity(ALICE_NAME, 70) override val bankOfCorda = TestIdentity(BOC_NAME) override val bigCorp = TestIdentity(CordaX500Name("BigCorporation", "New York", "US")) @@ -135,12 +137,22 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties { override fun before() { - // register additional identities - val databaseAndServices = makeTestDatabaseAndMockServices( - cordappPackages, - makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), - megaCorp, - moreKeys = *arrayOf(DUMMY_NOTARY_KEY)) + val databaseAndServices = if (persistentServices) { + makeTestDatabaseAndPersistentServices( + cordappPackages, + megaCorp, + moreKeys = setOf(DUMMY_NOTARY_KEY), + moreIdentities = setOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity) + ) + } else { + @Suppress("SpreadOperator") + makeTestDatabaseAndMockServices( + cordappPackages, + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), + megaCorp, + moreKeys = *arrayOf(DUMMY_NOTARY_KEY) + ) + } database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) @@ -2832,9 +2844,8 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate { - companion object { - val delegate = VaultQueryTestRule() + val delegate = VaultQueryTestRule(persistentServices = false) } @Rule @@ -3137,4 +3148,34 @@ class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate { ) } } -} \ No newline at end of file +} + + +class PersistentServicesVaultQueryTests : VaultQueryParties by delegate { + companion object { + val delegate = VaultQueryTestRule(persistentServices = true) + + @ClassRule + @JvmField + val testSerialization = SerializationEnvironmentRule() + } + + @Rule + @JvmField + val vaultQueryTestRule = delegate + + @Test(timeout = 300_000) + fun `query on externalId which maps to multiple keys`() { + val externalId = UUID.randomUUID() + val page = database.transaction { + val keys = Array(2) { services.keyManagementService.freshKey(externalId) } + vaultFiller.fillWithDummyState(participants = keys.map(::AnonymousParty)) + services.vaultService.queryBy( + VaultQueryCriteria(externalIds = listOf(externalId)), + paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + ) + } + assertThat(page.states).hasSize(1) + assertThat(page.totalStatesAvailable).isEqualTo(1) + } +} diff --git a/node/src/test/kotlin/net/corda/node/utilities/InfrequentlyMutatedCacheTest.kt b/node/src/test/kotlin/net/corda/node/utilities/InfrequentlyMutatedCacheTest.kt index fab9b1d7be..bfc6a377f6 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/InfrequentlyMutatedCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/InfrequentlyMutatedCacheTest.kt @@ -25,6 +25,11 @@ class InfrequentlyMutatedCacheTest { database.close() } + @Test(timeout = 300_000) + fun `invalidate outside transaction should not hang`() { + cache.invalidate("Fred") + } + @Test(timeout=300_000) fun `get from empty cache returns result of loader`() { database.transaction { diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index c3aeafa1b9..02fa732184 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -31,6 +31,9 @@ configurations { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" compile "javax.servlet:javax.servlet-api:${servlet_version}" @@ -77,6 +80,7 @@ def webTask = tasks.getByPath(':testing:testserver:testcapsule::assemble') task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow", "InvokeRpc.partiesFromName", + "InvokeRpc.notaryPartyFromX500Name", "InvokeRpc.attachmentExists", "InvokeRpc.openAttachment", "InvokeRpc.uploadAttachment", diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index 290452c9de..b60e813471 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -5,10 +5,13 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.all import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec import net.corda.testing.node.User +import net.corda.testing.node.internal.DummyClusterSpec import net.corda.testing.node.internal.findCordapp import org.junit.Test import java.util.concurrent.CompletableFuture.supplyAsync @@ -21,7 +24,8 @@ class AttachmentDemoTest { driver(DriverParameters( portAllocation = incrementalPortAllocation(), startNodesInProcess = true, - cordappsForAllNodes = listOf(findCordapp("net.corda.attachmentdemo.contracts"), findCordapp("net.corda.attachmentdemo.workflows"))) + cordappsForAllNodes = listOf(findCordapp("net.corda.attachmentdemo.contracts"), findCordapp("net.corda.attachmentdemo.workflows")), + notarySpecs = listOf(NotarySpec(name = DUMMY_NOTARY_NAME, cluster = DummyClusterSpec(clusterSize = 1)))) ) { val demoUser = listOf(User("demo", "demo", setOf(all()))) val (nodeA, nodeB) = listOf( diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index ebe4d8e1e8..2851a3c654 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -5,6 +5,7 @@ import net.corda.attachmentdemo.contracts.AttachmentContract import net.corda.attachmentdemo.workflows.AttachmentDemoFlow import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.Emoji import net.corda.core.internal.InputStreamAndHash import net.corda.core.messaging.CordaRPCOps @@ -65,7 +66,7 @@ fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K. private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256) { // Get the identity key of the other side (the recipient). - val notaryParty = rpc.partiesFromName("Notary", false).firstOrNull() ?: throw IllegalArgumentException("Couldn't find notary party") + val notaryParty = rpc.notaryPartyFromX500Name(CordaX500Name.parse("O=Notary Service,L=Zurich,C=CH")) ?: throw IllegalArgumentException("Couldn't find notary party") val bankBParty = rpc.partiesFromName("Bank B", false).firstOrNull() ?: throw IllegalArgumentException("Couldn't find Bank B party") // Make sure we have the file in storage if (!rpc.attachmentExists(hash)) { diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index aa377a6cee..11d398ce8b 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -6,6 +6,9 @@ apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // The bank of corda CorDapp depends upon Cash CorDapp features diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index 4c57c1b941..5f1155c184 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -3,6 +3,9 @@ apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordformation' dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } runtimeOnly project(':node-api') // Cordformation needs a SLF4J implementation when executing the Network // Bootstrapper, but Log4J doesn't shutdown completely from within Gradle. diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 700dd73dc4..0cf5896f9a 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -9,7 +9,8 @@ plugins { ext['artemis.version'] = "$artemis_version" ext['hibernate.version'] = "$hibernate_version" ext['selenium.version'] = "$selenium_version" -ext['jackson.version'] = "$jackson_version" +// Using jackson_kotlin_version here for JDK11 compatibility with kotlin 1.2.71 +ext['jackson.version'] = "$jackson_kotlin_version" ext['dropwizard-metrics.version'] = "$metrics_version" ext['mockito.version'] = "$mockito_version" diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index e46aea2330..71d0428949 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -27,6 +27,9 @@ cordapp { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } cordapp project(':finance:contracts') cordapp project(':finance:workflows') diff --git a/samples/network-verifier/build.gradle b/samples/network-verifier/build.gradle index ab6d4d37a5..1cd1c9f4b8 100644 --- a/samples/network-verifier/build.gradle +++ b/samples/network-verifier/build.gradle @@ -12,6 +12,9 @@ cordapp { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } // Cordformation needs this for the Network Bootstrapper. runtimeOnly project(':node-api') diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 1cd9710a65..6f87b55b35 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -15,6 +15,9 @@ cordapp { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" cordaCompile project(':client:rpc') // Corda integration dependencies diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 7f2f3e17bc..0609747f93 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -26,8 +26,10 @@ configurations { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - // The SIMM demo CorDapp depends upon Cash CorDapp features cordapp project(':finance:contracts') cordapp project(':finance:workflows') diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 68cffdff69..0b5272f7a7 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -32,6 +32,9 @@ configurations { } dependencies { + if (System.getProperty('excludeShell') == null) { + cordaDriver "net.corda:corda-shell:$corda_release_version" + } compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" cordaCompile project(':client:rpc') diff --git a/settings.gradle b/settings.gradle index bad03080bb..d24cdc802f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -74,8 +74,6 @@ include 'tools:loadtest' include 'tools:graphs' include 'tools:bootstrapper' include 'tools:blobinspector' -include 'tools:shell' -include 'tools:shell-cli' include 'tools:network-builder' include 'tools:cliutils' include 'tools:worldmap' diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index 7f3b3be7ee..772813c41b 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -27,6 +27,8 @@ sourceSets { dependencies { compile project(':test-utils') + compile group: 'org.apache.sshd', name: 'sshd-common', version: '2.9.2' + // Integration test helpers testCompile "org.assertj:assertj-core:$assertj_version" integrationTestImplementation "junit:junit:$junit_version" diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index cfd06a585b..c0f0e6a120 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -29,6 +29,7 @@ import rx.Observable import java.io.File import java.nio.file.Path import java.nio.file.Paths +import java.time.Duration import java.util.concurrent.atomic.AtomicInteger /** @@ -216,7 +217,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, - premigrateH2Database = defaultParameters.premigrateH2Database + premigrateH2Database = defaultParameters.premigrateH2Database, + notaryHandleTimeout = defaultParameters.notaryHandleTimeout ), coerce = { it }, dsl = dsl @@ -256,6 +258,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr * @property djvmBootstrapSource Location of a JAR containing the Java APIs for the DJVM to use. * @property djvmCordaSource Locations of JARs of user-supplied classes to execute within the DJVM sandbox. * @property premigrateH2Database Whether to use a prebuilt H2 database schema or start from an empty schema. + * @property notaryHandleTimeout Specifies how long to wait to receive a notary handle. This waiting includes waiting for + * the notary to start. * This can save time for tests which do not need to migrate from a blank schema. */ @Suppress("unused") @@ -281,7 +285,8 @@ data class DriverParameters( val djvmCordaSource: List = emptyList(), val environmentVariables: Map = emptyMap(), val allowHibernateToManageAppSchema: Boolean = true, - val premigrateH2Database: Boolean = true + val premigrateH2Database: Boolean = true, + val notaryHandleTimeout: Duration = Duration.ofMinutes(1) ) { constructor(cordappsForAllNodes: Collection) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes) @@ -464,6 +469,51 @@ data class DriverParameters( cordappsForAllNodes = null ) + constructor( + isDebug: Boolean, + driverDirectory: Path, + portAllocation: PortAllocation, + debugPortAllocation: PortAllocation, + systemProperties: Map, + useTestClock: Boolean, + startNodesInProcess: Boolean, + waitForAllNodesToFinish: Boolean, + notarySpecs: List, + extraCordappPackagesToScan: List, + jmxPolicy: JmxPolicy, + networkParameters: NetworkParameters, + notaryCustomOverrides: Map, + inMemoryDB: Boolean, + cordappsForAllNodes: Collection?, + djvmBootstrapSource: Path?, + djvmCordaSource: List, + environmentVariables: Map, + allowHibernateToManageAppSchema: Boolean, + premigrateH2Database: Boolean = true + ) : this( + isDebug, + driverDirectory, + portAllocation, + debugPortAllocation, + systemProperties, + useTestClock, + startNodesInProcess, + waitForAllNodesToFinish, + notarySpecs, + extraCordappPackagesToScan, + jmxPolicy, + networkParameters, + notaryCustomOverrides, + inMemoryDB, + cordappsForAllNodes, + djvmBootstrapSource, + djvmCordaSource, + environmentVariables, + allowHibernateToManageAppSchema, + premigrateH2Database, + notaryHandleTimeout = Duration.ofMinutes(1) + ) + fun withIsDebug(isDebug: Boolean): DriverParameters = copy(isDebug = isDebug) fun withDriverDirectory(driverDirectory: Path): DriverParameters = copy(driverDirectory = driverDirectory) fun withPortAllocation(portAllocation: PortAllocation): DriverParameters = copy(portAllocation = portAllocation) @@ -487,6 +537,7 @@ data class DriverParameters( fun withDjvmCordaSource(djvmCordaSource: List): DriverParameters = copy(djvmCordaSource = djvmCordaSource) fun withEnvironmentVariables(variables: Map): DriverParameters = copy(environmentVariables = variables) fun withAllowHibernateToManageAppSchema(value: Boolean): DriverParameters = copy(allowHibernateToManageAppSchema = value) + fun withNotaryHandleTimeout(value: Duration): DriverParameters = copy(notaryHandleTimeout = value) fun copy( isDebug: Boolean, @@ -631,4 +682,50 @@ data class DriverParameters( allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, premigrateH2Database = true ) + + @Suppress("LongParameterList") + fun copy( + isDebug: Boolean, + driverDirectory: Path, + portAllocation: PortAllocation, + debugPortAllocation: PortAllocation, + systemProperties: Map, + useTestClock: Boolean, + startNodesInProcess: Boolean, + waitForAllNodesToFinish: Boolean, + notarySpecs: List, + extraCordappPackagesToScan: List, + jmxPolicy: JmxPolicy, + networkParameters: NetworkParameters, + notaryCustomOverrides: Map, + inMemoryDB: Boolean, + cordappsForAllNodes: Collection?, + djvmBootstrapSource: Path?, + djvmCordaSource: List, + environmentVariables: Map, + allowHibernateToManageAppSchema: Boolean, + premigrateH2Database: Boolean + ) = this.copy( + isDebug = isDebug, + driverDirectory = driverDirectory, + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + useTestClock = useTestClock, + startNodesInProcess = startNodesInProcess, + waitForAllNodesToFinish = waitForAllNodesToFinish, + notarySpecs = notarySpecs, + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy, + networkParameters = networkParameters, + notaryCustomOverrides = notaryCustomOverrides, + inMemoryDB = inMemoryDB, + cordappsForAllNodes = cordappsForAllNodes, + djvmBootstrapSource = djvmBootstrapSource, + djvmCordaSource = djvmCordaSource, + environmentVariables = environmentVariables, + allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, + premigrateH2Database = premigrateH2Database, + notaryHandleTimeout = Duration.ofMinutes(1) + ) } \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/checkpoint/CheckpointRpcHelper.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/checkpoint/CheckpointRpcHelper.kt index 867abe4794..c83a813750 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/checkpoint/CheckpointRpcHelper.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/checkpoint/CheckpointRpcHelper.kt @@ -1,6 +1,6 @@ package net.corda.testing.driver.internal.checkpoint -import net.corda.core.internal.messaging.FlowManagerRPCOps +import net.corda.core.messaging.flows.FlowManagerRPCOps import net.corda.testing.driver.NodeHandle object CheckpointRpcHelper { 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 8f1c33c953..5dc744d1bf 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 @@ -154,7 +154,8 @@ class DriverDSLImpl( val djvmCordaSource: List, val environmentVariables: Map, val allowHibernateToManageAppSchema: Boolean = true, - val premigrateH2Database: Boolean = true + val premigrateH2Database: Boolean = true, + val notaryHandleTimeout: Duration = Duration.ofMinutes(1) ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null @@ -854,7 +855,6 @@ class DriverDSLImpl( // While starting with inProcess mode, we need to have different names to avoid clashes private val inMemoryCounter = AtomicInteger() - private val notaryHandleTimeout = Duration.ofMinutes(1) private val defaultRpcUserList = listOf(InternalUser("default", "default", setOf("ALL")).toConfig().root().unwrapped()) private val names = arrayOf(ALICE_NAME, BOB_NAME, DUMMY_BANK_A_NAME) @@ -1332,7 +1332,8 @@ fun genericDriver( djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, - premigrateH2Database = defaultParameters.premigrateH2Database + premigrateH2Database = defaultParameters.premigrateH2Database, + notaryHandleTimeout = defaultParameters.notaryHandleTimeout ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1432,6 +1433,7 @@ fun internalDriver( environmentVariables: Map = emptyMap(), allowHibernateToManageAppSchema: Boolean = true, premigrateH2Database: Boolean = true, + notaryHandleTimeout: Duration = Duration.ofMinutes(1), dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -1456,7 +1458,8 @@ fun internalDriver( djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, - premigrateH2Database = premigrateH2Database + premigrateH2Database = premigrateH2Database, + notaryHandleTimeout = notaryHandleTimeout ), coerce = { it }, dsl = dsl diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 73cb5dbc83..1d8106993d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -35,6 +35,7 @@ import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.fromUserList import net.corda.testing.node.NotarySpec import net.corda.testing.node.User +import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -42,7 +43,6 @@ import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BA import org.apache.activemq.artemis.api.core.client.ClientSession import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.core.config.Configuration -import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory @@ -201,30 +201,18 @@ data class RPCDriverDSL( journalBufferSize_NIO = maxFileSize journalBufferSize_AIO = maxFileSize journalFileSize = maxFileSize - queueConfigurations = listOf( - CoreQueueConfiguration().apply { - name = RPCApi.RPC_SERVER_QUEUE_NAME - address = RPCApi.RPC_SERVER_QUEUE_NAME - isDurable = false - }, - CoreQueueConfiguration().apply { - name = RPCApi.RPC_CLIENT_BINDING_REMOVALS - address = notificationAddress - filterString = RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION - isDurable = false - }, - CoreQueueConfiguration().apply { - name = RPCApi.RPC_CLIENT_BINDING_ADDITIONS - address = notificationAddress - filterString = RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION - isDurable = false - } + queueConfigs = listOf( + QueueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME).setAddress(RPCApi.RPC_SERVER_QUEUE_NAME).setDurable(false), + QueueConfiguration(RPCApi.RPC_CLIENT_BINDING_REMOVALS).setAddress(notificationAddress) + .setFilterString(RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION).setDurable(false), + QueueConfiguration(RPCApi.RPC_CLIENT_BINDING_ADDITIONS).setAddress(notificationAddress) + .setFilterString(RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION).setDurable(false) ) addressesSettings = mapOf( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply { maxSizeBytes = maxBufferedBytesPerClient addressFullMessagePolicy = AddressFullMessagePolicy.PAGE - pageSizeBytes = maxSizeBytes / 10 + pageSizeBytes = maxSizeBytes.toInt() / 10 } ) } @@ -259,7 +247,7 @@ data class RPCDriverDSL( * Starts an In-VM RPC server. Note that only a single one may be started. * * @param rpcUser The single user who can access the server through RPC, and their permissions. - * @param nodeLegalName The legal name of the node to check against to authenticate a super user. + * @param nodeLegalName The legal name of the node to check against to authenticate a superuser. * @param configuration The RPC server configuration. * @param ops The server-side implementation of the RPC interface. */ @@ -338,7 +326,7 @@ data class RPCDriverDSL( * * @param serverName The name of the server, to be used for the folder created for Artemis files. * @param rpcUser The single user who can access the server through RPC, and their permissions. - * @param nodeLegalName The legal name of the node to check against to authenticate a super user. + * @param nodeLegalName The legal name of the node to check against to authenticate a superuser. * @param configuration The RPC server configuration. * @param listOps The server-side implementation of the RPC interfaces. */ diff --git a/testing/test-common/src/main/resources/log4j2-test.xml b/testing/test-common/src/main/resources/log4j2-test.xml index 45910c8ca5..12041ff680 100644 --- a/testing/test-common/src/main/resources/log4j2-test.xml +++ b/testing/test-common/src/main/resources/log4j2-test.xml @@ -90,7 +90,15 @@ - + + + + + + + + + diff --git a/tools/checkpoint-agent/src/main/kotlin/net/corda/tools/CheckpointAgent.kt b/tools/checkpoint-agent/src/main/kotlin/net/corda/tools/CheckpointAgent.kt index 9fb3aa2b84..f6d5eb41cc 100644 --- a/tools/checkpoint-agent/src/main/kotlin/net/corda/tools/CheckpointAgent.kt +++ b/tools/checkpoint-agent/src/main/kotlin/net/corda/tools/CheckpointAgent.kt @@ -306,8 +306,9 @@ object CheckpointHook : ClassFileTransformer { (input.total() >= minimumSize) && (input.total() <= maximumSize))) { val sb = StringBuilder() - if (checkpointId != null) + if (checkpointId != null) { sb.append("Checkpoint id: $checkpointId\n") + } prettyStatsTree(0, StatsInfo("", Any::class.java), readTree(list, 0).second, sb) log.info("[READ] $clazz\n$sb") @@ -329,7 +330,7 @@ object CheckpointHook : ClassFileTransformer { @JvmStatic fun writeExit(output: Output, obj: Any) { val (list, count) = events[-Strand.currentStrand().id]!! - list.add(StatsEvent.Exit(obj.javaClass.name, output.total(), null)) + list.add(StatsEvent.Exit(obj.javaClass.name, output.total(), obj)) log.debug { "writeExit: clazz[${obj.javaClass.name}], strandId[${Strand.currentStrand().id}], eventCount[$count]" } if (count.decrementAndGet() == 0) { // always log diagnostics for explicit checkpoint ids (eg. set dumpCheckpoints) @@ -338,6 +339,9 @@ object CheckpointHook : ClassFileTransformer { (output.total() >= minimumSize) && (output.total() <= maximumSize))) { val sb = StringBuilder() + if (checkpointId != null) { + sb.append("Checkpoint id: $checkpointId\n") + } prettyStatsTree(0, StatsInfo("", Any::class.java), readTree(list, 0).second, sb) log.info("[WRITE] $obj\n$sb") checkpointId = null @@ -433,7 +437,7 @@ fun readTree(events: List, index: Int, idMap: IdentityHashMap() - - commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } - cordappDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } - user?.apply { cmdOpts["node.user"] = this } - password?.apply { cmdOpts["node.password"] = this } - host?.apply { cmdOpts["node.addresses.rpc.host"] = this } - port?.apply { cmdOpts["node.addresses.rpc.port"] = this } - trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } - trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } - trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } - - return ConfigFactory.parseMap(cmdOpts) - } - - /** Return configuration parsed from an optional config file (provided by the command line option) - * and then overridden by the command line options */ - fun toConfig(): ShellConfiguration { - val fileConfig = configFile?.let { ConfigFactory.parseFile(it.toFile()) } - ?: ConfigFactory.empty() - val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() - val shellConfigFile = typeSafeConfig.parseAs() - return shellConfigFile.toShellConfiguration() - } -} - -/** Object representation of Shell configuration file */ -private class ShellConfigurationFile { - data class Rpc( - val host: String, - val port: Int) - - data class Addresses( - val rpc: Rpc - ) - - data class Node( - val addresses: Addresses, - val user: String?, - val password: String? - ) - - data class Cordapps( - val path: String - ) - - data class Commands( - val path: String - ) - - data class Extensions( - val cordapps: Cordapps?, - val commands: Commands? - ) - - data class KeyStore( - val path: String, - val type: String = "JKS", - val password: String - ) - - data class Ssl( - val truststore: KeyStore - ) - - data class ShellConfigFile( - val node: Node, - val extensions: Extensions?, - val ssl: Ssl? - ) { - fun toShellConfiguration(): ShellConfiguration { - - val sslOptions = - ssl?.let { - ClientRpcSslOptions( - trustStorePath = Paths.get(it.truststore.path), - trustStorePassword = it.truststore.password) - } - - return ShellConfiguration( - commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") - / COMMANDS_DIR, - cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, - user = node.user ?: "", - password = node.password ?: "", - hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), - ssl = sslOptions) - } - } -} diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt deleted file mode 100644 index 1d2c7daa3f..0000000000 --- a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt +++ /dev/null @@ -1,113 +0,0 @@ -package net.corda.tools.shell - -import com.jcabi.manifests.Manifests -import net.corda.cliutils.CordaCliWrapper -import net.corda.cliutils.ExitCodes -import net.corda.cliutils.start -import net.corda.core.internal.exists -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.list -import org.fusesource.jansi.Ansi -import org.fusesource.jansi.AnsiConsole -import org.slf4j.bridge.SLF4JBridgeHandler -import picocli.CommandLine.Mixin -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.net.URL -import java.net.URLClassLoader -import java.nio.file.Path -import java.util.concurrent.CountDownLatch -import kotlin.streams.toList - -fun main(args: Array) { - StandaloneShell().start(args) -} - -class StandaloneShell : CordaCliWrapper("corda-shell", "The Corda standalone shell.") { - @Mixin - var cmdLineOptions = ShellCmdLineOptions() - - lateinit var configuration: ShellConfiguration - - private fun getCordappsInDirectory(cordappsDir: Path?): List = - if (cordappsDir == null || !cordappsDir.exists()) { - emptyList() - } else { - cordappsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() - } - } - - //Workaround in case console is not available - @Throws(IOException::class) - private fun readLine(format: String, vararg args: Any): String { - if (System.console() != null) { - return System.console().readLine(format, *args) - } - print(String.format(format, *args)) - val reader = BufferedReader(InputStreamReader(System.`in`)) - return reader.readLine() - } - - @Throws(IOException::class) - private fun readPassword(format: String, vararg args: Any) = - if (System.console() != null) System.console().readPassword(format, *args) else this.readLine(format, *args).toCharArray() - - private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown" - - override fun initLogging() : Boolean { - super.initLogging() - SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. - SLF4JBridgeHandler.install() - return true - } - - override fun runProgram(): Int { - configuration = try { - cmdLineOptions.toConfig() - } catch(e: Exception) { - println("Configuration exception: ${e.message}") - return ExitCodes.FAILURE - } - - val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory) - val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) - with(configuration) { - if (user.isEmpty()) { - user = readLine("User:") - } - if (password.isEmpty()) { - password = String(readPassword("Password:")) - } - } - InteractiveShell.startShell(configuration, classLoader, true) - try { - //connecting to node by requesting node info to fail fast - InteractiveShell.nodeInfo() - } catch (e: Exception) { - println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"") - return ExitCodes.FAILURE - } - - val exit = CountDownLatch(1) - AnsiConsole.systemInstall() - println(Ansi.ansi().fgBrightRed().a( - """ ______ __""").newline().a( - """ / ____/ _________/ /___ _""").newline().a( - """ / / __ / ___/ __ / __ `/ """).newline().fgBrightRed().a( - """/ /___ /_/ / / / /_/ / /_/ /""").newline().fgBrightRed().a( - """\____/ /_/ \__,_/\__,_/""").reset().fgBrightDefault().bold() - .newline().a("--- ${getManifestEntry("Corda-Vendor")} ${getManifestEntry("Corda-Release-Version")} (${getManifestEntry("Corda-Revision").take(7)}) ---") - .newline() - .newline().a("Standalone Shell connected to ${configuration.hostAndPort}") - .reset()) - InteractiveShell.runLocalShell { - exit.countDown() - } - - exit.await() - // because we can't clean certain Crash Shell threads that block on read() - return ExitCodes.SUCCESS - } -} diff --git a/tools/shell-cli/src/main/resources/net/corda/tools/shell/base/login.groovy b/tools/shell-cli/src/main/resources/net/corda/tools/shell/base/login.groovy deleted file mode 100644 index cb886c05bc..0000000000 --- a/tools/shell-cli/src/main/resources/net/corda/tools/shell/base/login.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.tools.shell.base - -// Note that this file MUST be in a sub-directory called "base" relative to the path -// given in the configuration code in InteractiveShell. - -// Copy of the login.groovy file from 'shell' module with the welcome tailored for the standalone shell -welcome = """ - -Welcome to the Corda interactive shell. -Useful commands include 'help' to see what is available, and 'bye' to exit the shell. - -""" - -prompt = { -> - return "${new Date()}>>> " -} diff --git a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt deleted file mode 100644 index 523ef5e879..0000000000 --- a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ /dev/null @@ -1,118 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.internal.toPath -import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.core.utilities.NetworkHostAndPort -import org.junit.Test -import java.nio.file.Paths -import kotlin.test.assertEquals - -class StandaloneShellArgsParserTest { - private val CONFIG_FILE = StandaloneShellArgsParserTest::class.java.getResource("/config.conf").toPath() - - @Test(timeout=300_000) - fun empty_args_to_cmd_options() { - val expectedOptions = ShellCmdLineOptions() - - assertEquals(expectedOptions.configFile, null) - assertEquals(expectedOptions.cordappDirectory, null) - assertEquals(expectedOptions.commandsDirectory, null) - assertEquals(expectedOptions.host, null) - assertEquals(expectedOptions.port, null) - assertEquals(expectedOptions.user, null) - assertEquals(expectedOptions.password, null) - } - - @Test(timeout=300_000) - fun args_to_config() { - val options = ShellCmdLineOptions() - options.configFile = null - options.commandsDirectory = Paths.get("/x/y/commands") - options.cordappDirectory = Paths.get("/x/y/cordapps") - options.host = "alocalhost" - options.port = "1234" - options.user = "demo" - options.password = "abcd1234" - options.trustStorePassword = "pass2" - options.trustStoreFile = Paths.get("/x/y/truststore.jks") - options.trustStoreType = "dummy" - - val expectedSsl = ClientRpcSslOptions( - trustStorePath = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2") - val expectedConfig = ShellConfiguration( - commandsDirectory = Paths.get("/x/y/commands"), - cordappsDirectory = Paths.get("/x/y/cordapps"), - user = "demo", - password = "abcd1234", - hostAndPort = NetworkHostAndPort("alocalhost", 1234), - ssl = expectedSsl, - sshdPort = null, - sshHostKeyDirectory = null, - noLocalShell = false) - - val config = options.toConfig() - - assertEquals(expectedConfig, config) - } - - @Test(timeout=300_000) - fun cmd_options_to_config_from_file() { - val options = ShellCmdLineOptions() - options.configFile = CONFIG_FILE - options.commandsDirectory = null - options.cordappDirectory = null - options.host = null - options.port = null - options.user = null - options.password = null - options.trustStorePassword = null - options.trustStoreFile = null - options.trustStoreType = null - - val expectedConfig = ShellConfiguration( - commandsDirectory = Paths.get("/x/y/commands"), - cordappsDirectory = Paths.get("/x/y/cordapps"), - user = "demo", - password = "abcd1234", - hostAndPort = NetworkHostAndPort("alocalhost", 1234), - ssl = ClientRpcSslOptions( - trustStorePath = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2"), - sshdPort = null) - - val config = options.toConfig() - - assertEquals(expectedConfig, config) - } - - @Test(timeout=300_000) - fun cmd_options_override_config_from_file() { - val options = ShellCmdLineOptions() - options.configFile = CONFIG_FILE - options.commandsDirectory = null - options.host = null - options.port = null - options.user = null - options.password = "blabla" - options.trustStorePassword = null - options.trustStoreFile = null - options.trustStoreType = null - - val expectedSsl = ClientRpcSslOptions( - trustStorePath = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2") - val expectedConfig = ShellConfiguration( - commandsDirectory = Paths.get("/x/y/commands"), - cordappsDirectory = Paths.get("/x/y/cordapps"), - user = "demo", - password = "blabla", - hostAndPort = NetworkHostAndPort("alocalhost", 1234), - ssl = expectedSsl, - sshdPort = null) - - val config = options.toConfig() - - assertEquals(expectedConfig, config) - } -} \ No newline at end of file diff --git a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt deleted file mode 100644 index 3f56ef218f..0000000000 --- a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.tools.shell - -import net.corda.testing.CliBackwardsCompatibleTest - -class StandaloneShellCompatibilityTest : CliBackwardsCompatibleTest(StandaloneShell::class.java) - diff --git a/tools/shell-cli/src/test/resources/config.conf b/tools/shell-cli/src/test/resources/config.conf deleted file mode 100644 index 2bfab9f0a3..0000000000 --- a/tools/shell-cli/src/test/resources/config.conf +++ /dev/null @@ -1,25 +0,0 @@ -node { - addresses { - rpc { - host : "alocalhost" - port : 1234 - } - } - user : demo - password : abcd1234 -} -extensions { - cordapps { - path : "/x/y/cordapps" - } - commands { - path : /x/y/commands - } -} -ssl { - truststore { - path : "/x/y/truststore.jks" - type : "JKS" - password : "pass2" - } - } \ No newline at end of file diff --git a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml deleted file mode 100644 index 75e4310091..0000000000 --- a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml +++ /dev/null @@ -1,104 +0,0 @@ -- commandName: "
" - positionalParams: [] - params: - - parameterName: "--commands-directory" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "--config-file" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "--cordapp-directory" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "--host" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--log-to-console" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--logging-level" - parameterType: "org.slf4j.event.Level" - required: false - multiParam: false - acceptableValues: - - "ERROR" - - "WARN" - - "INFO" - - "DEBUG" - - "TRACE" - - parameterName: "--password" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--port" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--truststore-file" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "--truststore-password" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--truststore-type" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--user" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "--verbose" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "-a" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "-c" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "-f" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "-o" - parameterType: "java.nio.file.Path" - required: false - multiParam: true - acceptableValues: [] - - parameterName: "-p" - parameterType: "java.lang.String" - required: false - multiParam: false - acceptableValues: [] - - parameterName: "-v" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle deleted file mode 100644 index 4e6f55fda7..0000000000 --- a/tools/shell/build.gradle +++ /dev/null @@ -1,90 +0,0 @@ -apply plugin: 'kotlin' -apply plugin: 'java' -apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'Corda Shell' - -configurations { - integrationTestCompile.extendsFrom testCompile - integrationTestRuntimeOnly.extendsFrom testRuntimeOnly -} - -sourceSets { - integrationTest { - kotlin { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/kotlin') - } - resources { - srcDir file('src/integration-test/resources') - } - } -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - - compile project(':node-api') - compile project(':client:rpc') - - // Jackson support: serialisation to/from JSON, YAML, etc. - compile project(':client:jackson') - - // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy. - compile("org.crashub:crash.shell:$crash_version") { - exclude group: "org.slf4j", module: "slf4j-jdk14" - exclude group: "org.bouncycastle" - } - - compile("org.crashub:crash.connectors.ssh:$crash_version") { - exclude group: "org.slf4j", module: "slf4j-jdk14" - exclude group: "org.bouncycastle" - } - - // JAnsi: for drawing things to the terminal in nicely coloured ways. - compile "org.fusesource.jansi:jansi:$jansi_version" - - // Manifests: for reading stuff from the manifest file. - compile "com.jcabi:jcabi-manifests:1.1" - - // For logging, required for ANSIProgressRenderer. - compile "org.apache.logging.log4j:log4j-core:$log4j_version" - - testImplementation "junit:junit:$junit_version" - - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" - testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" - - // Unit testing helpers. - testCompile "org.assertj:assertj-core:$assertj_version" - testCompile project(':test-utils') - testCompile project(':finance:contracts') - testCompile project(':finance:workflows') - - // Jsh: Testing SSH server. - integrationTestCompile "com.jcraft:jsch:$jsch_version" - - integrationTestCompile project(':node-driver') -} - -tasks.withType(JavaCompile).configureEach { - // Resolves a Gradle warning about not scanning for pre-processors. - options.compilerArgs << '-proc:none' -} - -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath -} - -jar { - baseName 'corda-shell' -} - -publish { - name jar.baseName -} diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt deleted file mode 100644 index 21852cb9ca..0000000000 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ /dev/null @@ -1,577 +0,0 @@ -package net.corda.tools.shell - -import co.paralleluniverse.fibers.Suspendable -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.type.TypeFactory -import com.jcraft.jsch.ChannelExec -import com.jcraft.jsch.JSch -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doAnswer -import com.nhaarman.mockito_kotlin.mock -import net.corda.client.jackson.JacksonSupport -import net.corda.client.jackson.internal.valueAs -import net.corda.client.rpc.RPCException -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.* -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.internal.concurrent.transpose -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.inputStream -import net.corda.core.internal.list -import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.startFlow -import net.corda.core.node.ServiceHub -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.seconds -import net.corda.node.internal.NodeStartup -import net.corda.node.services.Permissions -import net.corda.node.services.Permissions.Companion.all -import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.node.services.config.shell.toShellConfig -import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate -import net.corda.node.utilities.saveToKeyStore -import net.corda.node.utilities.saveToTrustStore -import net.corda.nodeapi.BrokerRpcSslOptions -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.NodeHandle -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.NodeHandleInternal -import net.corda.testing.driver.internal.checkpoint.CheckpointRpcHelper.checkpointsRpc -import net.corda.testing.internal.useSslRpcOverrides -import net.corda.testing.node.User -import net.corda.testing.node.internal.enclosedCordapp -import net.corda.tools.shell.SSHServerTest.FlowICanRun -import net.corda.tools.shell.utlities.ANSIProgressRenderer -import org.apache.activemq.artemis.api.core.ActiveMQSecurityException -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.bouncycastle.util.io.Streams -import org.crsh.text.RenderPrintWriter -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.jupiter.api.assertThrows -import org.junit.rules.TemporaryFolder -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeoutException -import java.util.zip.ZipInputStream -import javax.security.auth.x500.X500Principal -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class InteractiveShellIntegrationTest { - @Rule - @JvmField - val tempFolder = TemporaryFolder() - - private val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") - - private lateinit var inputObjectMapper: ObjectMapper - - @Before - fun setup() { - inputObjectMapper = objectMapperWithClassLoader(InteractiveShell.getCordappsClassloader()) - } - - @Test(timeout=300_000) - fun `shell should not log in with invalid credentials`() { - val user = User("u", "p", setOf()) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell("fake", "fake", node.rpcAddress) - assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java) - } - } - - @Test(timeout=300_000) - fun `shell should log in with valid credentials`() { - val user = User("u", "p", setOf()) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node) - InteractiveShell.nodeInfo() - } - } - - @Test(timeout=300_000) - fun `shell should log in with ssl`() { - val user = User("mark", "dadada", setOf(all())) - - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName) - val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert) - val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") - - val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert) - val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() - startShell(node, clientSslOptions) - InteractiveShell.nodeInfo() - } - } - - @Test(timeout=300_000) - fun `shell shoud not log in with invalid truststore`() { - val user = User("mark", "dadada", setOf("ALL")) - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName) - val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert) - val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") - - val (_, cert1) = createKeyPairAndSelfSignedTLSCertificate(testName) - val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1) - val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() - startShell(node, clientSslOptions) - assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(RPCException::class.java) - } - } - - @Test(timeout=300_000) - fun `internal shell user should not be able to connect if node started with devMode=false`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode().getOrThrow() - val conf = (node as NodeHandleInternal).configuration.toShellConfig() - InteractiveShell.startShell(conf) - assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java) - } - } - - @Ignore - @Test(timeout=300_000) - fun `ssh runs flows via standalone shell`() { - val user = User("u", "p", setOf( - startFlow(), - Permissions.invokeRpc(CordaRPCOps::registeredFlows), - Permissions.invokeRpc(CordaRPCOps::nodeInfo) - )) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node, sshdPort = 2224) - InteractiveShell.nodeInfo() - - val session = JSch().getSession("u", "localhost", 2224) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - session.connect() - - assertTrue(session.isConnected) - - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand("start FlowICanRun") - channel.connect(5000) - - assertTrue(channel.isConnected) - - val response = String(Streams.readAll(channel.inputStream)) - - val linesWithDoneCount = response.lines().filter { line -> "Done" in line } - - channel.disconnect() - session.disconnect() - - // There are ANSI control characters involved, so we want to avoid direct byte to byte matching. - assertThat(linesWithDoneCount).hasSize(1) - } - } - - @Ignore - @Test(timeout=300_000) - fun `ssh run flows via standalone shell over ssl to node`() { - val user = User("mark", "dadada", setOf( - startFlow(), - Permissions.invokeRpc(CordaRPCOps::registeredFlows), - Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/ - )) - - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName) - val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert) - val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") - val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert) - val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - - var successful = false - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> - startShell(node, clientSslOptions, sshdPort = 2223) - InteractiveShell.nodeInfo() - - val session = JSch().getSession("mark", "localhost", 2223) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("dadada") - session.connect() - - assertTrue(session.isConnected) - - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand("start FlowICanRun") - channel.connect(5000) - - assertTrue(channel.isConnected) - - val response = String(Streams.readAll(channel.inputStream)) - - val linesWithDoneCount = response.lines().filter { line -> "Done" in line } - - channel.disconnect() - session.disconnect() // TODO Simon make sure to close them - - // There are ANSI control characters involved, so we want to avoid direct byte to byte matching. - assertThat(linesWithDoneCount).hasSize(1) - - successful = true - } - - assertThat(successful).isTrue() - } - } - - @Test(timeout=300_000) - fun `shell should start flow with fully qualified class name`() { - val user = User("u", "p", setOf(all())) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node) - val (output, lines) = mockRenderPrintWriter() - InteractiveShell.runFlowByNameFragment(NoOpFlow::class.java.name, "", output, node.rpc, mockAnsiProgressRenderer()) - assertThat(lines.last()).startsWith("Flow completed with result:") - } - } - - @Test(timeout=300_000) - fun `shell should start flow with unique un-qualified class name`() { - val user = User("u", "p", setOf(all())) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node) - val (output, lines) = mockRenderPrintWriter() - InteractiveShell.runFlowByNameFragment("NoOpFlowA", "", output, node.rpc, mockAnsiProgressRenderer()) - assertThat(lines.last()).startsWith("Flow completed with result:") - } - } - - @Test(timeout=300_000) - fun `shell should fail to start flow with ambiguous class name`() { - val user = User("u", "p", setOf(all())) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node) - val (output, lines) = mockRenderPrintWriter() - InteractiveShell.runFlowByNameFragment("NoOpFlo", "", output, node.rpc, mockAnsiProgressRenderer()) - assertThat(lines.any { it.startsWith("Ambiguous name provided, please be more specific.") }).isTrue() - } - } - - @Test(timeout=300_000) - fun `shell should start flow with partially matching class name`() { - val user = User("u", "p", setOf(all())) - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - startShell(node) - val (output, lines) = mockRenderPrintWriter() - InteractiveShell.runFlowByNameFragment("Burble", "", output, node.rpc, mockAnsiProgressRenderer()) - assertThat(lines.last()).startsWith("Flow completed with result") - } - } - - @Test(timeout=300_000) - fun `dumpCheckpoints correctly serializes FlowExternalOperations`() { - driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories() - alice.rpc.startFlow(::ExternalOperationFlow) - ExternalOperation.lock.acquire() - alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) } - ExternalOperation.lock2.release() - - val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() } - val json = ZipInputStream(zipFile.inputStream()).use { zip -> - zip.nextEntry - ObjectMapper().readTree(zip) - } - - assertEquals("hello there", json["suspendedOn"]["customOperation"]["operation"]["a"].asText()) - assertEquals(123, json["suspendedOn"]["customOperation"]["operation"]["b"].asInt()) - assertEquals("please work", json["suspendedOn"]["customOperation"]["operation"]["c"]["d"].asText()) - assertEquals("I beg you", json["suspendedOn"]["customOperation"]["operation"]["c"]["e"].asText()) - } - } - - @Test(timeout=300_000) - fun `dumpCheckpoints correctly serializes FlowExternalAsyncOperations`() { - driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories() - alice.rpc.startFlow(::ExternalAsyncOperationFlow) - ExternalAsyncOperation.lock.acquire() - alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) } - ExternalAsyncOperation.future.complete(null) - val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() } - val json = ZipInputStream(zipFile.inputStream()).use { zip -> - zip.nextEntry - ObjectMapper().readTree(zip) - } - - assertEquals("hello there", json["suspendedOn"]["customOperation"]["operation"]["a"].asText()) - assertEquals(123, json["suspendedOn"]["customOperation"]["operation"]["b"].asInt()) - assertEquals("please work", json["suspendedOn"]["customOperation"]["operation"]["c"]["d"].asText()) - assertEquals("I beg you", json["suspendedOn"]["customOperation"]["operation"]["c"]["e"].asText()) - } - } - - @Test(timeout=300_000) - fun `dumpCheckpoints correctly serializes WaitForStateConsumption`() { - driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories() - val stateRefs = setOf( - StateRef(SecureHash.randomSHA256(), 0), - StateRef(SecureHash.randomSHA256(), 1), - StateRef(SecureHash.randomSHA256(), 2) - ) - assertThrows { - alice.rpc.startFlow(::WaitForStateConsumptionFlow, stateRefs).returnValue.getOrThrow(10.seconds) - } - alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) } - val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() } - val json = ZipInputStream(zipFile.inputStream()).use { zip -> - zip.nextEntry - ObjectMapper().readTree(zip) - } - - assertEquals(stateRefs, json["suspendedOn"]["waitForStateConsumption"].valueAs>(inputObjectMapper).toSet()) - } - } - - @Test(timeout=300_000) - fun `dumpCheckpoints creates zip with json file for suspended flow`() { - val user = User("u", "p", setOf(all())) - driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(enclosedCordapp()))) { - val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME) - .map { startNode(providedName = it, rpcUsers = listOf(user)) } - .transpose() - .getOrThrow() - bobNode.stop() - - // Create logs directory since the driver is not creating it - (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories() - - startShell(aliceNode) - - val linearId = UniqueIdentifier(id = UUID.fromString("7c0719f0-e489-46e8-bf3b-ee203156fc7c")) - aliceNode.rpc.startFlow( - ::FlowForCheckpointDumping, - MyState( - "some random string", - linearId, - listOf(aliceNode.nodeInfo.singleIdentity(), bobNode.nodeInfo.singleIdentity()) - ), - bobNode.nodeInfo.singleIdentity() - ) - - Thread.sleep(5000) - - mockRenderPrintWriter() - aliceNode.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) } - - val zipFile = (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() } - val json = ZipInputStream(zipFile.inputStream()).use { zip -> - zip.nextEntry - ObjectMapper().readTree(zip) - } - - assertNotNull(json["flowId"].asText()) - assertEquals(FlowForCheckpointDumping::class.java.name, json["topLevelFlowClass"].asText()) - assertEquals(linearId.id.toString(), json["topLevelFlowLogic"]["myState"]["linearId"]["id"].asText()) - assertEquals(4, json["flowCallStackSummary"].size()) - assertEquals(4, json["flowCallStack"].size()) - val sendAndReceiveJson = json["suspendedOn"]["sendAndReceive"][0] - assertEquals(bobNode.nodeInfo.singleIdentity().toString(), sendAndReceiveJson["session"]["peer"].asText()) - assertEquals(SignedTransaction::class.qualifiedName, sendAndReceiveJson["sentPayloadType"].asText()) - } - } - - private fun startShell(node: NodeHandle, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) { - val user = node.rpcUsers[0] - startShell(user.username, user.password, node.rpcAddress, ssl, sshdPort) - } - - private fun startShell(user: String, password: String, address: NetworkHostAndPort, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) { - val conf = ShellConfiguration( - commandsDirectory = tempFolder.newFolder().toPath(), - user = user, - password = password, - hostAndPort = address, - ssl = ssl, - sshdPort = sshdPort - ) - InteractiveShell.startShell(conf) - } - - private fun mockRenderPrintWriter(): Pair> { - val lines = ArrayList() - val writer = mock { - on { println(any()) } doAnswer { - val line = it.getArgument(0, String::class.java) - println(">>> $line") - lines += line - Unit - } - } - return Pair(writer, lines) - } - - private fun mockAnsiProgressRenderer(): ANSIProgressRenderer { - return mock { - on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } - } - } - - private fun objectMapperWithClassLoader(classLoader: ClassLoader?): ObjectMapper { - val objectMapper = JacksonSupport.createNonRpcMapper() - val tf = TypeFactory.defaultInstance().withClassLoader(classLoader) - objectMapper.typeFactory = tf - return objectMapper - } - - @Suppress("UNUSED") - @StartableByRPC - class NoOpFlow : FlowLogic() { - override val progressTracker = ProgressTracker() - override fun call() { - println("NO OP!") - } - } - - @Suppress("UNUSED") - @StartableByRPC - class NoOpFlowA : FlowLogic() { - override val progressTracker = ProgressTracker() - override fun call() { - println("NO OP! (A)") - } - } - - @Suppress("UNUSED") - @StartableByRPC - class BurbleFlow : FlowLogic() { - override val progressTracker = ProgressTracker() - override fun call() { - println("NO OP! (Burble)") - } - } - - @InitiatingFlow - @StartableByRPC - class FlowForCheckpointDumping(private val myState: MyState, private val party: Party): FlowLogic() { - // Make sure any SerializeAsToken instances are not serialised - private var services: ServiceHub? = null - - @Suspendable - override fun call() { - services = serviceHub - val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { - addOutputState(myState) - addCommand(MyContract.Create(), listOf(ourIdentity, party).map(Party::owningKey)) - } - val sessions = listOf(initiateFlow(party)) - val stx = serviceHub.signInitialTransaction(tx) - subFlow(CollectSignaturesFlow(stx, sessions)) - throw IllegalStateException("The test should not get here") - } - } - - @InitiatedBy(FlowForCheckpointDumping::class) - class FlowForCheckpointDumpingResponder(private val session: FlowSession): FlowLogic() { - override fun call() { - val signTxFlow = object : SignTransactionFlow(session) { - override fun checkTransaction(stx: SignedTransaction) { - - } - } - subFlow(signTxFlow) - throw IllegalStateException("The test should not get here") - } - } - - class MyContract : Contract { - class Create : CommandData - override fun verify(tx: LedgerTransaction) {} - } - - @BelongsToContract(MyContract::class) - data class MyState( - val data: String, - override val linearId: UniqueIdentifier, - override val participants: List - ) : LinearState - - @StartableByRPC - class ExternalAsyncOperationFlow : FlowLogic() { - @Suspendable - override fun call() { - await(ExternalAsyncOperation("hello there", 123, Data("please work", "I beg you"))) - } - } - - class ExternalAsyncOperation(val a: String, val b: Int, val c: Data): FlowExternalAsyncOperation { - - companion object { - val future = CompletableFuture() - val lock = Semaphore(0) - } - - override fun execute(deduplicationId: String): CompletableFuture { - return future.also { lock.release() } - } - } - - class Data(val d: String, val e: String) - - @StartableByRPC - class ExternalOperationFlow : FlowLogic() { - @Suspendable - override fun call() { - await(ExternalOperation("hello there", 123, Data("please work", "I beg you"))) - } - } - - class ExternalOperation(val a: String, val b: Int, val c: Data): FlowExternalOperation { - - companion object { - val lock = Semaphore(0) - val lock2 = Semaphore(0) - } - - override fun execute(deduplicationId: String) { - lock.release() - lock2.acquire() - } - } - - @StartableByRPC - class WaitForStateConsumptionFlow(private val stateRefs: Set) : FlowLogic() { - @Suspendable - override fun call() { - waitForStateConsumption(stateRefs) - } - } -} \ No newline at end of file diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt deleted file mode 100644 index 74011aa7c8..0000000000 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/SSHServerTest.kt +++ /dev/null @@ -1,184 +0,0 @@ -package net.corda.tools.shell - -import co.paralleluniverse.fibers.Suspendable -import com.jcraft.jsch.ChannelExec -import com.jcraft.jsch.JSch -import com.jcraft.jsch.JSchException -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.Party -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.unwrap -import net.corda.node.services.Permissions.Companion.invokeRpc -import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.node.User -import net.corda.testing.node.internal.enclosedCordapp -import org.assertj.core.api.Assertions.assertThat -import org.bouncycastle.util.io.Streams -import org.junit.Ignore -import org.junit.Test -import java.net.ConnectException -import kotlin.test.assertTrue -import kotlin.test.fail - -class SSHServerTest { - @Test(timeout=300_000) - fun `ssh server does not start by default`() { - val user = User("u", "p", setOf()) - // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)) - node.getOrThrow() - - val session = JSch().getSession("u", "localhost", 2222) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - - try { - session.connect() - fail() - } catch (e: JSchException) { - assertTrue(e.cause is ConnectException) - } - } - } - - @Test(timeout=300_000) - fun `ssh server starts when configured`() { - val user = User("u", "p", setOf()) - // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), - customOverrides = mapOf("sshd" to mapOf("port" to 2222)) /*, startInSameProcess = true */) - node.getOrThrow() - - val session = JSch().getSession("u", "localhost", 2222) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - - session.connect() - - assertTrue(session.isConnected) - } - } - - @Test(timeout=300_000) - fun `ssh server verify credentials`() { - val user = User("u", "p", setOf()) - // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), - customOverrides = mapOf("sshd" to mapOf("port" to 2222))) - node.getOrThrow() - - val session = JSch().getSession("u", "localhost", 2222) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p_is_bad_password") - - try { - session.connect() - fail("Server should reject invalid credentials") - } catch (e: JSchException) { - //There is no specialized exception for this - assertTrue(e.message == "Auth fail") - } - } - } - - @Test(timeout=300_000) - fun `ssh respects permissions`() { - val user = User("u", "p", setOf(startFlow(), - invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name))) - // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), - customOverrides = mapOf("sshd" to mapOf("port" to 2222))) - node.getOrThrow() - - val session = JSch().getSession("u", "localhost", 2222) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - session.connect() - - assertTrue(session.isConnected) - - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand("start FlowICannotRun otherParty: \"$ALICE_NAME\"") - channel.connect() - val response = String(Streams.readAll(channel.inputStream)) - - channel.disconnect() - session.disconnect() - - assertThat(response).matches("(?s)User not authorized to perform RPC call .*") - } - } - - @Ignore - @Test(timeout=300_000) - fun `ssh runs flows`() { - val user = User("u", "p", setOf(startFlow())) - // The driver will automatically pick up the annotated flows below - driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) { - val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), - customOverrides = mapOf("sshd" to mapOf("port" to 2222))) - node.getOrThrow() - - val session = JSch().getSession("u", "localhost", 2222) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("p") - session.connect() - - assertTrue(session.isConnected) - - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand("start FlowICanRun") - channel.connect(5000) - - assertTrue(channel.isConnected) - - val response = String(Streams.readAll(channel.inputStream)) - - val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") } - - channel.disconnect() - session.disconnect() - - // There are ANSI control characters involved, so we want to avoid direct byte to byte matching. - assertThat(linesWithDoneCount).size().isGreaterThanOrEqualTo(1) - } - } - - @StartableByRPC - @InitiatingFlow - class FlowICanRun : FlowLogic() { - - private val HELLO_STEP = ProgressTracker.Step("Hello") - - @Suspendable - override fun call(): String { - progressTracker?.currentStep = HELLO_STEP - return "bambam" - } - - override val progressTracker: ProgressTracker? = ProgressTracker(HELLO_STEP) - } - - @Suppress("unused") - @StartableByRPC - @InitiatingFlow - class FlowICannotRun(private val otherParty: Party) : FlowLogic() { - @Suspendable - override fun call(): String = initiateFlow(otherParty).receive().unwrap { it } - - override val progressTracker: ProgressTracker? = ProgressTracker() - } - - -} \ No newline at end of file diff --git a/tools/shell/src/integration-test/resources/ssl.conf b/tools/shell/src/integration-test/resources/ssl.conf deleted file mode 100644 index f8faaa8788..0000000000 --- a/tools/shell/src/integration-test/resources/ssl.conf +++ /dev/null @@ -1,8 +0,0 @@ -user=demo1 -baseDirectory="/Users/szymonsztuka/Documents/shell-config" -hostAndPort="localhost:10006" -sshdPort=2223 -ssl { - keyStorePassword=password - trustStorePassword=password -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/AttachmentShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/AttachmentShellCommand.java deleted file mode 100644 index c5f4a5f602..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/AttachmentShellCommand.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.corda.tools.shell; - -import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps; -import org.crsh.cli.Command; -import org.crsh.cli.Man; -import org.crsh.cli.Named; -import org.crsh.cli.Usage; -import org.jetbrains.annotations.NotNull; - -import static net.corda.tools.shell.InteractiveShell.runAttachmentTrustInfoView; - -@Named("attachments") -public class AttachmentShellCommand extends InteractiveShellCommand { - - @NotNull - @Override - public Class getRpcOpsClass() { - return AttachmentTrustInfoRPCOps.class; - } - - @Command - @Man("Displays the trusted CorDapp attachments that have been manually installed or received over the network") - @Usage("Displays the trusted CorDapp attachments that have been manually installed or received over the network") - public void trustInfo() { - runAttachmentTrustInfoView(out, ops()); - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/CheckpointShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/CheckpointShellCommand.java deleted file mode 100644 index 30b3cf7ae6..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/CheckpointShellCommand.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.tools.shell; - -import net.corda.core.internal.messaging.FlowManagerRPCOps; -import org.crsh.cli.Command; -import org.crsh.cli.Man; -import org.crsh.cli.Named; -import org.crsh.cli.Usage; -import org.jetbrains.annotations.NotNull; - -import static net.corda.tools.shell.InteractiveShell.*; - -@Named("checkpoints") -public class CheckpointShellCommand extends InteractiveShellCommand { - - @NotNull - @Override - public Class getRpcOpsClass() { - return FlowManagerRPCOps.class; - } - - @Command - @Man("Outputs the contents of all checkpoints as json to be manually reviewed") - @Usage("Outputs the contents of all checkpoints as json to be manually reviewed") - public void dump() { - runDumpCheckpoints(ops()); - } - - @Command - @Man("Outputs the contents of all started flow checkpoints in a zip file") - @Usage("Outputs the contents of all started flow checkpoints in a zip file") - public void debug() { - runDebugCheckpoints(ops()); - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java deleted file mode 100644 index 420cace42d..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.corda.tools.shell; - -// See the comments at the top of run.java - -import com.fasterxml.jackson.databind.ObjectMapper; -import net.corda.core.messaging.CordaRPCOps; -import net.corda.tools.shell.utlities.ANSIProgressRenderer; -import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer; -import org.crsh.cli.*; -import org.crsh.command.*; -import org.crsh.text.*; -import org.crsh.text.ui.TableElement; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; - -import static net.corda.tools.shell.InteractiveShell.killFlowById; -import static net.corda.tools.shell.InteractiveShell.runFlowByNameFragment; -import static net.corda.tools.shell.InteractiveShell.runStateMachinesView; - -@Man( - "Allows you to start and kill flows, list the ones available and to watch flows currently running on the node.\n\n" + - "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + - "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + - "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + - "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." -) -@Named("flow") -public class FlowShellCommand extends CordaRpcOpsShellCommand { - - private static final Logger logger = LoggerFactory.getLogger(FlowShellCommand.class); - - @Command - @Usage("Start a (work)flow on the node. This is how you can change the ledger.\n\n" + - "\t\t Starting flow is the primary way in which you command the node to change the ledger.\n" + - "\t\t This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + - "\t\t command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the\n" + - "\t\t flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command.\n") - public void start( - @Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, - @Usage("The data to pass as input") @Argument(unquote = false) List input - ) { - logger.info("Executing command \"flow start {} {}\",", name, (input != null) ? String.join(" ", input) : ""); - startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper(null)); - } - - // TODO Limit number of flows shown option? - @Command - @Usage("Watch information about state machines running on the node with result information.") - public void watch(InvocationContext context) throws Exception { - logger.info("Executing command \"flow watch\"."); - runStateMachinesView(out, ops()); - } - - static void startFlow(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, - @Usage("The data to pass as input") @Argument(unquote = false) List input, - RenderPrintWriter out, - CordaRPCOps rpcOps, - ANSIProgressRenderer ansiProgressRenderer, - ObjectMapper om) { - if (name == null) { - out.println("You must pass a name for the flow. Example: \"start Yo target: Some other company\"", Decoration.bold, Color.red); - return; - } - String inp = input == null ? "" : String.join(" ", input).trim(); - runFlowByNameFragment(name, inp, out, rpcOps, ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), om); - } - - @Command - @Usage("List flows that user can start.") - public void list(InvocationContext context) throws Exception { - logger.info("Executing command \"flow list\"."); - for (String name : ops().registeredFlows()) { - context.provide(name + System.lineSeparator()); - } - } - - @Command - @Usage("Kill a flow that is running on this node.") - public void kill( - @Usage("The UUID for the flow that we wish to kill") @Argument String id - ) { - logger.info("Executing command \"flow kill {}\".", id); - killFlowById(id, out, ops(), objectMapper(null)); - } -} \ No newline at end of file diff --git a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java deleted file mode 100644 index b5e72f82f6..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.corda.tools.shell; - -import net.corda.core.crypto.SecureHash; -import net.corda.core.crypto.SecureHashKt; -import net.corda.core.internal.VisibleForTesting; -import net.corda.core.messaging.CordaRPCOps; -import net.corda.core.messaging.StateMachineTransactionMapping; -import org.crsh.cli.Argument; -import org.crsh.cli.Command; -import org.crsh.cli.Man; -import org.crsh.cli.Named; -import org.crsh.cli.Usage; -import org.crsh.text.Color; -import org.crsh.text.Decoration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Optional; - -@Named("hashLookup") -public class HashLookupShellCommand extends CordaRpcOpsShellCommand { - private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class); - private static final String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" + - "Both the transaction Id and the hashed value of a transaction Id (as returned by the Notary in case of a double-spend) is a valid input.\n" + - "This is mainly intended to be used for troubleshooting notarisation issues when a\n" + - "state is claimed to be already consumed by another transaction.\n\n" + - "Example usage: hashLookup E470FD8A6350A74217B0A99EA5FB71F091C84C64AD0DE0E72ECC10421D03AAC9"; - - @Command - @Man(manualText) - - public void main(@Usage("A transaction Id or a hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) { - CordaRPCOps proxy = ops(); - try { - hashLookup(out, proxy, txIdHash); - } catch (IllegalArgumentException ex) { - out.println(manualText); - out.println(ex.getMessage(), Decoration.bold, Color.red); - } - } - - @VisibleForTesting - protected static void hashLookup(PrintWriter out, CordaRPCOps proxy, String txIdHash) throws IllegalArgumentException { - logger.info("Executing command \"hashLookup\"."); - - if (txIdHash == null) { - out.println(manualText); - throw new IllegalArgumentException("Please provide a hexadecimal transaction Id hash value or a transaction Id"); - } - - SecureHash txIdHashParsed; - try { - txIdHashParsed = SecureHash.create(txIdHash); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("The provided string is neither a valid SHA-256 hash value or a supported hash algorithm"); - } - - List mapping = proxy.stateMachineRecordedTransactionMappingSnapshot(); - Optional match = mapping.stream() - .map(StateMachineTransactionMapping::getTransactionId) - .filter( - txId -> txId.equals(txIdHashParsed) || SecureHash.hashAs(SecureHashKt.getAlgorithm(txIdHashParsed), txId.getBytes()).equals(txIdHashParsed) - ) - .findFirst(); - - if (match.isPresent()) { - SecureHash found = match.get(); - out.println("Found a matching transaction with Id: " + found.toString()); - } else { - throw new IllegalArgumentException("No matching transaction found"); - } - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/OutputFormatCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/OutputFormatCommand.java deleted file mode 100644 index 42fc3d8d77..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/OutputFormatCommand.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.tools.shell; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; -import net.corda.tools.shell.InteractiveShell.OutputFormat; -import org.crsh.cli.Argument; -import org.crsh.cli.Command; -import org.crsh.cli.Man; -import org.crsh.cli.Named; -import org.crsh.cli.Usage; -import org.crsh.command.InvocationContext; -import org.crsh.command.ScriptException; -import org.crsh.text.RenderPrintWriter; - -import java.util.Map; - -@Man("Allows you to see and update the format that's currently used for the commands' output.") -@Usage("Allows you to see and update the format that's currently used for the commands' output.") -@Named("output-format") -public class OutputFormatCommand extends CordaRpcOpsShellCommand { - - public OutputFormatCommand() {} - - @VisibleForTesting - OutputFormatCommand(final RenderPrintWriter printWriter) { - this.out = printWriter; - } - - private static final BiMap OUTPUT_FORMAT_MAPPING = ImmutableBiMap.of( - "json", OutputFormat.JSON, - "yaml", OutputFormat.YAML - ); - - @Command - @Man("Sets the output format of the commands.") - @Usage("sets the output format of the commands.") - public void set(InvocationContext context, - @Usage("The format of the commands output. Supported values: json, yaml.") @Argument String format) { - OutputFormat outputFormat = parseFormat(format); - - InteractiveShell.setOutputFormat(outputFormat); - } - - @Command - @Man("Shows the output format of the commands.") - @Usage("shows the output format of the commands.") - public void get(InvocationContext context) { - OutputFormat outputFormat = InteractiveShell.getOutputFormat(); - final String format = OUTPUT_FORMAT_MAPPING.inverse().get(outputFormat); - - out.println(format); - } - - private OutputFormat parseFormat(String format) { - if (!OUTPUT_FORMAT_MAPPING.containsKey(format)) { - throw new ScriptException("The provided format is not supported: " + format); - } - - return OUTPUT_FORMAT_MAPPING.get(format); - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java deleted file mode 100644 index 123e2510db..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java +++ /dev/null @@ -1,86 +0,0 @@ -package net.corda.tools.shell; - -import com.google.common.collect.Maps; -import net.corda.client.jackson.StringToMethodCallParser; -import net.corda.core.messaging.CordaRPCOps; -import org.crsh.cli.Argument; -import org.crsh.cli.Command; -import org.crsh.cli.Man; -import org.crsh.cli.Named; -import org.crsh.cli.Usage; -import org.crsh.command.InvocationContext; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static java.util.Comparator.comparing; - -// Note that this class cannot be converted to Kotlin because CRaSH does not understand InvocationContext> which -// is the closest you can get in Kotlin to raw types. - -@Named("run") -public class RunShellCommand extends CordaRpcOpsShellCommand { - - private static final Logger logger = LoggerFactory.getLogger(RunShellCommand.class); - - @Command - @Man( - "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + - - "You can learn more about what commands are available by typing 'run' just by itself, or by\n" + - "consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html" - ) - @Usage("runs a method from the CordaRPCOps interface on the node.") - public Object main(InvocationContext context, - @Usage("The command to run") @Argument(unquote = false) List command) { - logger.info("Executing command \"run {}\",", (command != null) ? String.join(" ", command) : ""); - - if (command == null) { - emitHelp(context); - return null; - } - - return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(InteractiveShell.getCordappsClassloader())); - } - - private void emitHelp(InvocationContext context) { - StringToMethodCallParser cordaRpcOpsParser = - new StringToMethodCallParser<>( - CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader())); - - // Sends data down the pipeline about what commands are available. CRaSH will render it nicely. - // Each element we emit is a map of column -> content. - Set> entries = cordaRpcOpsParser.getAvailableCommands().entrySet(); - List> entryList = new ArrayList<>(entries); - - entryList.add(new AbstractMap.SimpleEntry<>("gracefulShutdown", ""));//Shell only command - - entryList.sort(comparing(Map.Entry::getKey)); - for (Map.Entry entry : entryList) { - // Skip these entries as they aren't really interesting for the user. - if (entry.getKey().equals("startFlowDynamic")) continue; - if (entry.getKey().equals("getProtocolVersion")) continue; - - try { - context.provide(commandAndDesc(entry.getKey(), entry.getValue())); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - @NotNull - private Map commandAndDesc(String command, String description) { - // Use a LinkedHashMap to ensure that the Command column comes first. - Map abruptShutdown = Maps.newLinkedHashMap(); - abruptShutdown.put("Command", command); - abruptShutdown.put("Parameter types", description); - return abruptShutdown; - } -} diff --git a/tools/shell/src/main/java/net/corda/tools/shell/SshAuthInfo.java b/tools/shell/src/main/java/net/corda/tools/shell/SshAuthInfo.java deleted file mode 100644 index 7530ae7ba2..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/SshAuthInfo.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.corda.tools.shell; - -import net.corda.core.messaging.RPCOps; -import org.crsh.auth.AuthInfo; - -public interface SshAuthInfo extends AuthInfo { - T getOrCreateRpcOps(Class rpcOpsClass); -} \ No newline at end of file diff --git a/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java deleted file mode 100644 index 1245efdb02..0000000000 --- a/tools/shell/src/main/java/net/corda/tools/shell/StartShellCommand.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.corda.tools.shell; - -// A simple forwarder to the "flow start" command, for easier typing. - -import net.corda.tools.shell.utlities.ANSIProgressRenderer; -import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer; -import org.crsh.cli.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; - -import static java.util.stream.Collectors.joining; - -@Named("start") -public class StartShellCommand extends CordaRpcOpsShellCommand { - - private static Logger logger = LoggerFactory.getLogger(StartShellCommand.class); - - @Command - @Man("An alias for 'flow start'. Example: \"start Yo target: Some other company\"") - public void main(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, - @Usage("The data to pass as input") @Argument(unquote = false) List input) { - - logger.info("Executing command \"start {} {}\",", name, (input != null) ? input.stream().collect(joining(" ")) : ""); - ANSIProgressRenderer ansiProgressRenderer = ansiProgressRenderer(); - FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), objectMapper(null)); - } -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/AttachmentTrustTable.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/AttachmentTrustTable.kt deleted file mode 100644 index 67f71d19fb..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/AttachmentTrustTable.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.internal.AttachmentTrustInfo -import net.corda.core.internal.P2P_UPLOADER -import org.crsh.text.Color -import org.crsh.text.Decoration -import org.crsh.text.RenderPrintWriter -import org.crsh.text.ui.LabelElement -import org.crsh.text.ui.Overflow -import org.crsh.text.ui.RowElement -import org.crsh.text.ui.TableElement - -class AttachmentTrustTable( - writer: RenderPrintWriter, - private val attachmentTrustInfos: List -) { - - private val content: TableElement - - init { - content = createTable() - createRows() - writer.print(content) - } - - private fun createTable(): TableElement { - val table = TableElement(2, 3, 1, 1, 3).overflow(Overflow.WRAP).rightCellPadding(3) - val header = - RowElement(true).add("Name", "Attachment ID", "Installed", "Trusted", "Trust Root").style( - Decoration.bold.fg( - Color.black - ).bg(Color.white) - ) - table.add(header) - return table - } - - private fun createRows() { - for (info in attachmentTrustInfos) { - info.run { - val name = when { - fileName != null -> fileName!! - uploader?.startsWith(P2P_UPLOADER) ?: false -> { - "Received from: ${uploader!!.removePrefix("$P2P_UPLOADER:")}" - } - else -> "" - } - content.add( - RowElement().add( - LabelElement(name), - LabelElement(attachmentId), - LabelElement(isTrustRoot), - LabelElement(isTrusted), - LabelElement(trustRootFileName ?: trustRootId ?: "") - ) - ) - } - } - } -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt deleted file mode 100644 index 84c616e6e1..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaAuthenticationPlugin.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.utilities.loggerFor -import org.apache.activemq.artemis.api.core.ActiveMQSecurityException -import org.crsh.auth.AuthInfo -import org.crsh.auth.AuthenticationPlugin -import org.crsh.plugin.CRaSHPlugin - -internal class CordaAuthenticationPlugin(private val rpcOpsProducer: RPCOpsProducer) : CRaSHPlugin>(), AuthenticationPlugin { - - companion object { - private val logger = loggerFor() - } - - override fun getImplementation(): AuthenticationPlugin = this - - override fun getName(): String = "corda" - - override fun authenticate(username: String?, credential: String?): AuthInfo { - - if (username == null || credential == null) { - return AuthInfo.UNSUCCESSFUL - } - try { - val cordaSSHAuthInfo = CordaSSHAuthInfo(rpcOpsProducer, username, credential, isSsh = true) - // We cannot guarantee authentication happened successfully till `RCPClient` session been established, hence doing a dummy call - cordaSSHAuthInfo.getOrCreateRpcOps(CordaRPCOps::class.java).protocolVersion - return cordaSSHAuthInfo - } catch (e: ActiveMQSecurityException) { - logger.warn(e.message) - } catch (e: Exception) { - logger.warn(e.message, e) - } - return AuthInfo.UNSUCCESSFUL - } - - override fun getCredentialType(): Class = String::class.java -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaDisconnectPlugin.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaDisconnectPlugin.kt deleted file mode 100644 index 98e6d151cb..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaDisconnectPlugin.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.corda.tools.shell - -import org.crsh.auth.AuthInfo -import org.crsh.auth.DisconnectPlugin -import org.crsh.plugin.CRaSHPlugin - -class CordaDisconnectPlugin : CRaSHPlugin(), DisconnectPlugin { - override fun getImplementation() = this - - override fun onDisconnect(userName: String?, authInfo: AuthInfo?) { - (authInfo as? CordaSSHAuthInfo)?.cleanUp() - } -} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaRpcOpsShellCommand.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaRpcOpsShellCommand.kt deleted file mode 100644 index 430686a437..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaRpcOpsShellCommand.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.tools.shell - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.type.TypeFactory -import net.corda.core.messaging.CordaRPCOps - -internal abstract class CordaRpcOpsShellCommand : InteractiveShellCommand() { - override val rpcOpsClass: Class = CordaRPCOps::class.java - - fun objectMapper(classLoader: ClassLoader?): ObjectMapper { - val om = createYamlInputMapper() - if (classLoader != null) { - om.typeFactory = TypeFactory.defaultInstance().withClassLoader(classLoader) - } - return om - } - - private fun createYamlInputMapper(): ObjectMapper { - val rpcOps = ops() - return InteractiveShell.createYamlInputMapper(rpcOps) - } -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt deleted file mode 100644 index f8d91c75fd..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/CordaSSHAuthInfo.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.corda.tools.shell - -import com.github.benmanes.caffeine.cache.CacheLoader -import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.RemovalListener -import com.google.common.util.concurrent.MoreExecutors -import net.corda.client.rpc.RPCConnection -import net.corda.core.internal.utilities.InvocationHandlerTemplate -import net.corda.core.messaging.RPCOps -import net.corda.tools.shell.utlities.ANSIProgressRenderer -import java.lang.reflect.Proxy - -internal class CordaSSHAuthInfo(private val rpcOpsProducer: RPCOpsProducer, - private val username: String, private val credential: String, val ansiProgressRenderer: ANSIProgressRenderer? = null, - val isSsh: Boolean = false) : SshAuthInfo { - override fun isSuccessful(): Boolean = true - - /** - * It is necessary to have a cache to prevent creation of too many proxies for the same class. Proxy ensures that RPC connections gracefully - * closed when cache entry is eliminated - */ - private val proxiesCache = Caffeine.newBuilder() - .maximumSize(10) - .removalListener(RemovalListener, Pair>> { _, value, _ -> value?.second?.close() }) - .executor(MoreExecutors.directExecutor()) - .build(CacheLoader, Pair>> { key -> createRpcOps(key) }) - - override fun getOrCreateRpcOps(rpcOpsClass: Class): T { - @Suppress("UNCHECKED_CAST") - return proxiesCache.get(rpcOpsClass)!!.first as T - } - - fun cleanUp() { - proxiesCache.asMap().forEach { - proxiesCache.invalidate(it.key) - it.value.second.forceClose() - } - } - - private fun createRpcOps(rpcOpsClass: Class): Pair> { - val producerResult = rpcOpsProducer(username, credential, rpcOpsClass) - val anotherProxy = proxyRPCOps(producerResult.proxy, rpcOpsClass) - return anotherProxy to producerResult - } - - private fun proxyRPCOps(instance: T, rpcOpsClass: Class): T { - require(rpcOpsClass.isInterface) { "$rpcOpsClass must be an interface" } - @Suppress("UNCHECKED_CAST") - return Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), object : InvocationHandlerTemplate { - override val delegate = instance - }) as T - } -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt deleted file mode 100644 index e001425b0a..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/FlowWatchPrintingSubscriber.kt +++ /dev/null @@ -1,126 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.flows.StateMachineRunId -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.context.InvocationContext -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.StateMachineUpdate.Added -import net.corda.core.messaging.StateMachineUpdate.Removed -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.Try -import org.crsh.text.Color -import org.crsh.text.Decoration -import org.crsh.text.RenderPrintWriter -import org.crsh.text.ui.LabelElement -import org.crsh.text.ui.Overflow -import org.crsh.text.ui.RowElement -import org.crsh.text.ui.TableElement -import rx.Subscriber - -class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Subscriber() { - private val indexMap = HashMap() - private val table = createStateMachinesTable() - val future = openFuture() - - init { - // The future is public and can be completed by something else to indicate we don't wish to follow - // anymore (e.g. the user pressing Ctrl-C). - future.then { unsubscribe() } - } - - @Synchronized - override fun onCompleted() { - // The observable of state machines will never complete. - future.set(Unit) - } - - @Synchronized - override fun onNext(t: Any?) { - if (t is StateMachineUpdate) { - toStream.cls() - createStateMachinesRow(t) - toStream.print(table) - toStream.println("Waiting for completion or Ctrl-C ... ") - toStream.flush() - } - } - - @Synchronized - override fun onError(e: Throwable) { - toStream.println("Observable completed with an error") - future.setException(e) - } - - private fun stateColor(update: StateMachineUpdate): Color { - return when (update) { - is Added -> Color.blue - is Removed -> if (update.result.isSuccess) Color.green else Color.red - } - } - - private fun createStateMachinesTable(): TableElement { - val table = TableElement(1, 2, 1, 2).overflow(Overflow.HIDDEN).rightCellPadding(1) - val header = RowElement(true).add("Id", "Flow name", "Initiator", "Status").style(Decoration.bold.fg(Color.black).bg(Color.white)) - table.add(header) - return table - } - - // TODO Add progress tracker? - private fun createStateMachinesRow(smmUpdate: StateMachineUpdate) { - when (smmUpdate) { - is Added -> { - table.add(RowElement().add( - LabelElement(formatFlowId(smmUpdate.id)), - LabelElement(formatFlowName(smmUpdate.stateMachineInfo.flowLogicClassName)), - LabelElement(formatInvocationContext(smmUpdate.stateMachineInfo.invocationContext)), - LabelElement("In progress") - ).style(stateColor(smmUpdate).fg())) - indexMap[smmUpdate.id] = table.rows.size - 1 - } - is Removed -> { - val idx = indexMap[smmUpdate.id] - if (idx != null) { - val oldRow = table.rows[idx] - val flowNameLabel = oldRow.getCol(1) as LabelElement - val flowInitiatorLabel = oldRow.getCol(2) as LabelElement - table.rows[idx] = RowElement().add( - LabelElement(formatFlowId(smmUpdate.id)), - LabelElement(flowNameLabel.value), - LabelElement(flowInitiatorLabel.value), - LabelElement(formatFlowResult(smmUpdate.result)) - ).style(stateColor(smmUpdate).fg()) - } - } - } - } - - private fun formatFlowName(flowName: String): String { - val camelCaseRegex = Regex("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") - val name = flowName.split('.', '$').last() - // Split CamelCase and get rid of "flow" at the end if present. - return camelCaseRegex.split(name).filter { it.compareTo("Flow", true) != 0 }.joinToString(" ") - } - - private fun formatFlowId(flowId: StateMachineRunId): String { - return flowId.toString().removeSurrounding("[", "]") - } - - private fun formatInvocationContext(context: InvocationContext): String { - return context.principal().name - } - - private fun formatFlowResult(flowResult: Try<*>): String { - fun successFormat(value: Any?): String { - return when (value) { - is SignedTransaction -> "Tx ID: " + value.id.toString() - is kotlin.Unit -> "No return value" - null -> "No return value" - else -> value.toString() - } - } - return when (flowResult) { - is Try.Success -> successFormat(flowResult.value) - is Try.Failure -> flowResult.exception.message ?: flowResult.exception.toString() - } - } -} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt deleted file mode 100644 index ab8abadc26..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ /dev/null @@ -1,783 +0,0 @@ -package net.corda.tools.shell - -import com.fasterxml.jackson.core.JsonFactory -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator -import net.corda.client.jackson.JacksonSupport -import net.corda.client.jackson.StringToMethodCallParser -import net.corda.client.rpc.PermissionException -import net.corda.client.rpc.RPCConnection -import net.corda.client.rpc.internal.RPCUtils.isShutdownMethodName -import net.corda.client.rpc.notUsed -import net.corda.core.CordaException -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StateMachineRunId -import net.corda.core.internal.Emoji -import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.concurrent.doneFuture -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps -import net.corda.core.internal.messaging.FlowManagerRPCOps -import net.corda.core.internal.packageName_ -import net.corda.core.internal.rootCause -import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.FlowProgressHandle -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.pendingFlowsCount -import net.corda.tools.shell.utlities.ANSIProgressRenderer -import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer -import org.crsh.command.InvocationContext -import org.crsh.command.ShellSafety -import org.crsh.console.jline.JLineProcessor -import org.crsh.console.jline.TerminalFactory -import org.crsh.console.jline.console.ConsoleReader -import org.crsh.lang.impl.java.JavaLanguage -import org.crsh.plugin.CRaSHPlugin -import org.crsh.plugin.PluginContext -import org.crsh.plugin.PluginLifeCycle -import org.crsh.plugin.ServiceLoaderDiscovery -import org.crsh.shell.Shell -import org.crsh.shell.ShellFactory -import org.crsh.shell.impl.command.ExternalResolver -import org.crsh.text.Color -import org.crsh.text.Decoration -import org.crsh.text.RenderPrintWriter -import org.crsh.util.InterruptHandler -import org.crsh.util.Utils -import org.crsh.vfs.FS -import org.crsh.vfs.spi.file.FileMountFactory -import org.crsh.vfs.spi.url.ClassPathMountFactory -import org.slf4j.LoggerFactory -import rx.Observable -import rx.Subscriber -import java.io.FileDescriptor -import java.io.FileInputStream -import java.io.InputStream -import java.io.PrintWriter -import java.lang.reflect.GenericArrayType -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.UndeclaredThrowableException -import java.nio.file.Path -import java.util.Properties -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ExecutionException -import java.util.concurrent.Future -import kotlin.concurrent.thread - -// TODO: Add command history. -// TODO: Command completion. -// TODO: Do something sensible with commands that return a future. -// TODO: Configure default renderers, send objects down the pipeline, add support for xml output format. -// TODO: Add a command to view last N lines/tail/control log4j2 loggers. -// TODO: Review or fix the JVM commands which have bitrotted and some are useless. -// TODO: Get rid of the 'java' command, it's kind of worthless. -// TODO: Fix up the 'dashboard' command which has some rendering issues. -// TODO: Resurrect or reimplement the mail plugin. -// TODO: Make it notice new shell commands added after the node started. - -const val STANDALONE_SHELL_PERMISSION = "ALL" - -@Suppress("MaxLineLength") -object InteractiveShell { - private val log = LoggerFactory.getLogger(javaClass) - private lateinit var rpcOpsProducer: RPCOpsProducer - private lateinit var startupValidation: Lazy - private var rpcConn: RPCConnection? = null - private var shell: Shell? = null - private var classLoader: ClassLoader? = null - private lateinit var shellConfiguration: ShellConfiguration - private var onExit: () -> Unit = {} - private const val uuidStringSize = 36 - - @JvmStatic - fun getCordappsClassloader() = classLoader - - enum class OutputFormat { - JSON, - YAML - } - - fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null, standalone: Boolean = false) { - rpcOpsProducer = DefaultRPCOpsProducer(configuration, classLoader, standalone) - launchShell(configuration, standalone, classLoader) - } - - private fun launchShell(configuration: ShellConfiguration, standalone: Boolean, classLoader: ClassLoader? = null) { - shellConfiguration = configuration - InteractiveShell.classLoader = classLoader - val runSshDaemon = configuration.sshdPort != null - - var runShellInSafeMode = true - if (!standalone) { - log.info("launchShell: User=${configuration.user} perm=${configuration.permissions}") - log.info("Shell: PermitExit= ${configuration.localShellAllowExitInSafeMode}, UNSAFELOCAL=${configuration.localShellUnsafe}") - runShellInSafeMode = configuration.permissions?.filter { it.contains(STANDALONE_SHELL_PERMISSION); }?.isEmpty() != false - } - - val config = Properties() - if (runSshDaemon) { - // Enable SSH access. Note: these have to be strings, even though raw object assignments also work. - config["crash.ssh.port"] = configuration.sshdPort?.toString() - config["crash.auth"] = "corda" - configuration.sshHostKeyDirectory?.apply { - val sshKeysDir = configuration.sshHostKeyDirectory.createDirectories() - config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString() - config["crash.ssh.keygen"] = "true" - } - } - - ExternalResolver.INSTANCE.addCommand( - "output-format", - "Commands to inspect and update the output format.", - OutputFormatCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "run", - "Runs a method from the CordaRPCOps interface on the node.", - RunShellCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "flow", - "Commands to work with flows. Flows are how you can change the ledger.", - FlowShellCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "start", - "An alias for 'flow start'", - StartShellCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "hashLookup", - "Checks if a transaction with matching Id hash exists.", - HashLookupShellCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "attachments", - "Commands to extract information about attachments stored within the node", - AttachmentShellCommand::class.java - ) - ExternalResolver.INSTANCE.addCommand( - "checkpoints", - "Commands to extract information about checkpoints stored within the node", - CheckpointShellCommand::class.java - ) - - val shellSafety = ShellSafety().apply { - setSafeShell(runShellInSafeMode) - setInternal(!standalone) - setStandAlone(standalone) - setAllowExitInSafeMode(configuration.localShellAllowExitInSafeMode || standalone) - } - shell = ShellLifecycle(configuration.commandsDirectory, shellSafety).start(config, configuration.user, configuration.password) - } - - fun runLocalShell(onExit: () -> Unit = {}) { - this.onExit = onExit - val terminal = TerminalFactory.create() - val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) - val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) - InterruptHandler { jlineProcessor.interrupt() }.install() - thread(name = "Command line shell processor", isDaemon = true) { - Emoji.renderIfSupported { - try { - jlineProcessor.run() - } catch (e: IndexOutOfBoundsException) { - log.warn("Cannot parse malformed command.") - } - } - } - thread(name = "Command line shell terminator", isDaemon = true) { - // Wait for the shell to finish. - jlineProcessor.closed() - log.info("Command shell has exited") - terminal.restore() - onExit.invoke() - } - } - - class ShellLifecycle(private val shellCommands: Path, private val shellSafety: ShellSafety) : PluginLifeCycle() { - fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell { - val classLoader = this.javaClass.classLoader - val classpathDriver = ClassPathMountFactory(classLoader) - val fileDriver = FileMountFactory(Utils.getCurrentDirectory()) - - val extraCommandsPath = shellCommands.toAbsolutePath().createDirectories() - val commandsFS = FS.Builder() - .register("file", fileDriver) - .mount("file:$extraCommandsPath") - .register("classpath", classpathDriver) - .mount("classpath:/net/corda/tools/shell/") - .mount("classpath:/crash/commands/") - .build() - val confFS = FS.Builder() - .register("classpath", classpathDriver) - .mount("classpath:/crash") - .build() - - val discovery = object : ServiceLoaderDiscovery(classLoader) { - override fun getPlugins(): Iterable> { - // Don't use the Java language plugin (we may not have tools.jar available at runtime), this - // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that - // is only the 'jmx' command. - return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOpsProducer) + - CordaDisconnectPlugin() - } - } - val attributes = emptyMap() - val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader) - context.refresh() - this.config = config - start(context) - startupValidation = lazy { - rpcOpsProducer(localUserName, localUserPassword, CordaRPCOps::class.java).let { - rpcConn = it - it.proxy - } - } - // For local shell create an artificial authInfo with super user permissions - val authInfo = CordaSSHAuthInfo(rpcOpsProducer, localUserName, localUserPassword, StdoutANSIProgressRenderer) - return context.getPlugin(ShellFactory::class.java).create(null, authInfo, shellSafety) - } - } - - fun nodeInfo() = try { - startupValidation.value.nodeInfo() - } catch (e: UndeclaredThrowableException) { - throw e.cause ?: e - } - - @JvmStatic - fun setOutputFormat(outputFormat: OutputFormat) { - this.outputFormat = outputFormat - } - - @JvmStatic - fun getOutputFormat(): OutputFormat { - return outputFormat - } - - fun createYamlInputMapper(rpcOps: CordaRPCOps): ObjectMapper { - // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra - // serializers. - return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply { - val rpcModule = SimpleModule().apply { - addDeserializer(InputStream::class.java, InputStreamDeserializer) - addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) - } - registerModule(rpcModule) - } - } - - private fun createOutputMapper(outputFormat: OutputFormat): ObjectMapper { - val factory = when(outputFormat) { - OutputFormat.JSON -> JsonFactory() - OutputFormat.YAML -> YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) - } - - return JacksonSupport.createNonRpcMapper(factory).apply { - // Register serializers for stateful objects from libraries that are special to the RPC system and don't - // make sense to print out to the screen. For classes we own, annotations can be used instead. - val rpcModule = SimpleModule().apply { - addSerializer(Observable::class.java, ObservableSerializer) - addSerializer(InputStream::class.java, InputStreamSerializer) - } - registerModule(rpcModule) - - disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - enable(SerializationFeature.INDENT_OUTPUT) - } - } - - // TODO: A default renderer could be used, instead of an object mapper. See: http://www.crashub.org/1.3/reference.html#_renderers - private var outputFormat = OutputFormat.YAML - - @VisibleForTesting - lateinit var latch: CountDownLatch - private set - - /** - * Called from the 'flow' shell command. Takes a name fragment and finds a matching flow, or prints out - * the list of options if the request is ambiguous. Then parses [inputData] as constructor arguments using - * the [runFlowFromString] method and starts the requested flow. Ctrl-C can be used to cancel. - */ - @JvmStatic - fun runFlowByNameFragment(nameFragment: String, - inputData: String, - output: RenderPrintWriter, - rpcOps: CordaRPCOps, - ansiProgressRenderer: ANSIProgressRenderer, - inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) { - val matches = try { - rpcOps.registeredFlows().filter { nameFragment in it }.sortedBy { it.length } - } catch (e: PermissionException) { - output.println(e.message ?: "Access denied", Decoration.bold, Color.red) - return - } - if (matches.isEmpty()) { - output.println("No matching flow found, run 'flow list' to see your options.", Decoration.bold, Color.red) - return - } else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) { - output.println("Ambiguous name provided, please be more specific. Your options are:") - matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Decoration.bold, Color.yellow) } - return - } - - val flowName = matches.find { it.endsWith(nameFragment)} ?: matches.single() - val flowClazz: Class> = if (classLoader != null) { - uncheckedCast(Class.forName(flowName, true, classLoader)) - } else { - uncheckedCast(Class.forName(flowName)) - } - try { - // Show the progress tracker on the console until the flow completes or is interrupted with a - // Ctrl-C keypress. - val stateObservable = runFlowFromString( - { clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, - inputData, - flowClazz, - inputObjectMapper - ) - - latch = CountDownLatch(1) - ansiProgressRenderer.render(stateObservable, latch::countDown) - // Wait for the flow to end and the progress tracker to notice. By the time the latch is released - // the tracker is done with the screen. - while (!Thread.currentThread().isInterrupted) { - try { - latch.await() - break - } catch (e: InterruptedException) { - try { - rpcOps.killFlow(stateObservable.id) - } finally { - Thread.currentThread().interrupt() - break - } - } - } - output.println("Flow completed with result: ${stateObservable.returnValue.get()}") - } catch (e: NoApplicableConstructor) { - output.println("No matching constructor found:", Decoration.bold, Color.red) - e.errors.forEach { output.println("- $it", Decoration.bold, Color.red) } - } catch (e: PermissionException) { - output.println(e.message ?: "Access denied", Decoration.bold, Color.red) - } catch (e: ExecutionException) { - // ignoring it as already logged by the progress handler subscriber - } finally { - InputStreamDeserializer.closeAll() - } - } - - class NoApplicableConstructor(val errors: List) : CordaException(this.toString()) { - override fun toString() = - (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) - } - - /** - * Tidies up a possibly generic type name by chopping off the package names of classes in a hard-coded set of - * hierarchies that are known to be widely used and recognised, and also not have (m)any ambiguous names in them. - * - * This is used for printing error messages when something doesn't match. - */ - private fun maybeAbbreviateGenericType(type: Type, extraRecognisedPackage: String): String { - val packagesToAbbreviate = listOf("java.", "net.corda.core.", "kotlin.", extraRecognisedPackage) - - fun shouldAbbreviate(typeName: String) = packagesToAbbreviate.any { typeName.startsWith(it) } - fun abbreviated(typeName: String) = if (shouldAbbreviate(typeName)) typeName.split('.').last() else typeName - - fun innerLoop(type: Type): String = when (type) { - is ParameterizedType -> { - val args: List = type.actualTypeArguments.map(::innerLoop) - abbreviated(type.rawType.typeName) + '<' + args.joinToString(", ") + '>' - } - is GenericArrayType -> { - innerLoop(type.genericComponentType) + "[]" - } - is Class<*> -> { - if (type.isArray) - abbreviated(type.simpleName) - else - abbreviated(type.name).replace('$', '.') - } - else -> type.toString() - } - - return innerLoop(type) - } - - @JvmStatic - fun killFlowById(id: String, - output: RenderPrintWriter, - rpcOps: CordaRPCOps, - inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) { - try { - val runId = try { - inputObjectMapper.readValue(id, StateMachineRunId::class.java) - } catch (e: JsonMappingException) { - output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Decoration.bold, Color.red) - log.error("Failed to parse flow ID", e) - return - } - //auxiliary validation - workaround for JDK8 bug https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8159339 - if (id.length < uuidStringSize) { - val msg = "Can not kill the flow. Flow ID of '$id' seems to be malformed - a UUID should have $uuidStringSize characters. " + - "Expand the terminal window to see the full UUID value." - output.println(msg, Decoration.bold, Color.red) - log.warn(msg) - return - } - if (rpcOps.killFlow(runId)) { - output.println("Killed flow $runId", Decoration.bold, Color.yellow) - } else { - output.println("Failed to kill flow $runId", Decoration.bold, Color.red) - } - } finally { - output.flush() - } - } - - /** - * Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts - * the flow, returning the created flow logic. Useful for lightweight invocation where text is preferable - * to statically typed, compiled code. - * - * See the [StringToMethodCallParser] class to learn more about limitations and acceptable syntax. - * - * @throws NoApplicableConstructor if no constructor could be found for the given set of types. - */ - @Throws(NoApplicableConstructor::class) - fun runFlowFromString(invoke: (Class>, Array) -> FlowProgressHandle, - inputData: String, - clazz: Class>, - om: ObjectMapper): FlowProgressHandle { - - val errors = ArrayList() - val parser = StringToMethodCallParser(clazz, om) - val nameTypeList = getMatchingConstructorParamsAndTypes(parser, inputData, clazz) - - try { - val args = parser.parseArguments(clazz.name, nameTypeList, inputData) - return invoke(clazz, args) - } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { - val argTypes = nameTypeList.map { (_, type) -> type } - errors.add("$argTypes: ") - } catch (e: StringToMethodCallParser.UnparseableCallException) { - val argTypes = nameTypeList.map { (_, type) -> type } - errors.add("$argTypes: ${e.message}") - } - throw NoApplicableConstructor(errors) - } - - private fun getMatchingConstructorParamsAndTypes(parser: StringToMethodCallParser>, - inputData: String, - clazz: Class>) : List> { - val errors = ArrayList() - val classPackage = clazz.packageName_ - lateinit var paramNamesFromConstructor: List - - for (ctor in clazz.constructors) { // Attempt construction with the given arguments. - - fun getPrototype(): List { - val argTypes = ctor.genericParameterTypes.map { - // If the type name is in the net.corda.core or java namespaces, chop off the package name - // because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise. - maybeAbbreviateGenericType(it, classPackage) - } - return paramNamesFromConstructor.zip(argTypes).map { (name, type) -> "$name: $type" } - } - - try { - paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) - val nameTypeList = paramNamesFromConstructor.zip(ctor.genericParameterTypes) - parser.validateIsMatchingCtor(clazz.name, nameTypeList, inputData) - return nameTypeList - - } - catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { - errors.add("${getPrototype()}: missing parameter ${e.paramName}") - } - catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { - errors.add("${getPrototype()}: too many parameters") - } - catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { - val argTypes = ctor.genericParameterTypes.map { it.typeName } - errors.add("$argTypes: ") - } - catch (e: StringToMethodCallParser.UnparseableCallException) { - val argTypes = ctor.genericParameterTypes.map { it.typeName } - errors.add("$argTypes: ${e.message}") - } - } - throw NoApplicableConstructor(errors) - } - - // TODO Filtering on error/success when we will have some sort of flow auditing, for now it doesn't make much sense. - @JvmStatic - fun runStateMachinesView(out: RenderPrintWriter, rpcOps: CordaRPCOps): Any? { - val proxy = rpcOps - val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed() - val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) } - val subscriber = FlowWatchPrintingSubscriber(out) - stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber) - var result: Any? = subscriber.future - if (result is Future<*>) { - if (!result.isDone) { - out.cls() - out.println("Waiting for completion or Ctrl-C ... ") - out.flush() - } - try { - result = result.get() - } catch (e: InterruptedException) { - subscriber.unsubscribe() - Thread.currentThread().interrupt() - } catch (e: ExecutionException) { - throw e.rootCause - } catch (e: InvocationTargetException) { - throw e.rootCause - } - } - return result - } - - @JvmStatic - fun runAttachmentTrustInfoView( - out: RenderPrintWriter, - rpcOps: AttachmentTrustInfoRPCOps - ): Any { - return AttachmentTrustTable(out, rpcOps.attachmentTrustInfos) - } - - @JvmStatic - fun runDumpCheckpoints(rpcOps: FlowManagerRPCOps) { - rpcOps.dumpCheckpoints() - } - - @JvmStatic - fun runDebugCheckpoints(rpcOps: FlowManagerRPCOps) { - rpcOps.debugCheckpoints() - } - - @JvmStatic - fun runRPCFromString(input: List, out: RenderPrintWriter, context: InvocationContext, cordaRPCOps: CordaRPCOps, - inputObjectMapper: ObjectMapper): Any? { - val cmd = input.joinToString(" ").trim { it <= ' ' } - if (cmd.startsWith("startflow", ignoreCase = true)) { - // The flow command provides better support and startFlow requires special handling anyway due to - // the generic startFlow RPC interface which offers no type information with which to parse the - // string form of the command. - out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Decoration.bold, Color.yellow) - return null - } else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) { - return gracefulShutdown(out, cordaRPCOps) - } - - var result: Any? = null - try { - InputStreamSerializer.invokeContext = context - val parser = StringToMethodCallParser(CordaRPCOps::class.java, inputObjectMapper) - val call = parser.parse(cordaRPCOps, cmd) - result = call.call() - var subscription : Subscriber<*>? = null - if (result != null && result !== Unit && result !is Void) { - val (subs, future) = printAndFollowRPCResponse(result, out, outputFormat) - subscription = subs - result = future - } - if (result is Future<*>) { - if (!result.isDone) { - out.println("Waiting for completion or Ctrl-C ... ") - out.flush() - } - try { - result = result.get() - } catch (e: InterruptedException) { - subscription?.unsubscribe() - Thread.currentThread().interrupt() - } catch (e: ExecutionException) { - throw e.rootCause - } catch (e: InvocationTargetException) { - throw e.rootCause - } - } - if (isShutdownMethodName(cmd)) { - out.println("Called 'shutdown' on the node.\nQuitting the shell now.").also { out.flush() } - onExit.invoke() - } - } catch (e: StringToMethodCallParser.UnparseableCallException) { - out.println(e.message, Decoration.bold, Color.red) - if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) { - out.println("Please try 'run -h' to learn what syntax is acceptable") - } - } catch (e: Exception) { - out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red) - } finally { - InputStreamSerializer.invokeContext = null - InputStreamDeserializer.closeAll() - } - return result - } - - @JvmStatic - fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps): Int { - - var result = 0 // assume it all went well - - fun display(statements: RenderPrintWriter.() -> Unit) { - statements.invoke(userSessionOut) - userSessionOut.flush() - } - - try { - display { - println("Orchestrating a clean shutdown, press CTRL+C to cancel...") - println("...enabling draining mode") - println("...waiting for in-flight flows to be completed") - } - - val latch = CountDownLatch(1) - @Suppress("DEPRECATION") - val subscription = cordaRPCOps.pendingFlowsCount().updates - .doAfterTerminate(latch::countDown) - .subscribe( - // For each update. - { (completed, total) -> display { println("...remaining: $completed / $total") } }, - // On error. - { - log.error(it.message) - throw it - }, - // When completed. - { - // This will only show up in the standalone Shell, because the embedded one - // is killed as part of a node's shutdown. - display { println("...done, quitting the shell now.") } - } - ) - cordaRPCOps.terminate(true) - - try { - latch.await() - // Unsubscribe or we hold up the shutdown - subscription.unsubscribe() - rpcConn?.forceClose() - onExit.invoke() - } catch (e: InterruptedException) { - // Cancelled whilst draining flows. So let's carry on from here - cordaRPCOps.setFlowsDrainingModeEnabled(false) - display { println("...cancelled clean shutdown.") } - result = 1 - } - } catch (e: Exception) { - display { println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red) } - result = 1 - } finally { - InputStreamSerializer.invokeContext = null - InputStreamDeserializer.closeAll() - } - return result; - } - - private fun printAndFollowRPCResponse( - response: Any?, - out: PrintWriter, - outputFormat: OutputFormat - ): Pair> { - val outputMapper = createOutputMapper(outputFormat) - - val mapElement: (Any?) -> String = { element -> outputMapper.writerWithDefaultPrettyPrinter().writeValueAsString(element) } - return maybeFollow(response, mapElement, out) - } - - private class PrintingSubscriber(private val printerFun: (Any?) -> String, private val toStream: PrintWriter) : Subscriber() { - private var count = 0 - val future = openFuture() - - init { - // The future is public and can be completed by something else to indicate we don't wish to follow - // anymore (e.g. the user pressing Ctrl-C). - future.then { unsubscribe() } - } - - @Synchronized - override fun onCompleted() { - toStream.println("Observable has completed") - future.set(Unit) - } - - @Synchronized - override fun onNext(t: Any?) { - count++ - toStream.println("Observation $count: " + printerFun(t)) - toStream.flush() - } - - @Synchronized - override fun onError(e: Throwable) { - toStream.println("Observable completed with an error") - e.printStackTrace(toStream) - future.setException(e) - } - } - - private fun maybeFollow( - response: Any?, - printerFun: (Any?) -> String, - out: PrintWriter - ): Pair> { - // Match on a couple of common patterns for "important" observables. It's tough to do this in a generic - // way because observables can be embedded anywhere in the object graph, and can emit other arbitrary - // object graphs that contain yet more observables. So we just look for top level responses that follow - // the standard "track" pattern, and print them until the user presses Ctrl-C - var result = Pair>(null, doneFuture(Unit)) - - - when { - response is DataFeed<*, *> -> { - out.println("Snapshot:") - out.println(printerFun(response.snapshot)) - out.flush() - out.println("Updates:") - - val unsubscribeAndPrint: (Any?) -> String = { resp -> - if (resp is StateMachineUpdate.Added) { - resp.stateMachineInfo.progressTrackerStepAndUpdates?.updates?.notUsed() - } - printerFun(resp) - } - - result = printNextElements(response.updates, unsubscribeAndPrint, out) - } - response is Observable<*> -> { - result = printNextElements(response, printerFun, out) - } - response != null -> { - out.println(printerFun(response)) - } - } - return result - } - - private fun printNextElements( - elements: Observable<*>, - printerFun: (Any?) -> String, - out: PrintWriter - ): Pair> { - val subscriber = PrintingSubscriber(printerFun, out) - uncheckedCast(elements).subscribe(subscriber) - return Pair(subscriber, subscriber.future) - } - -} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt deleted file mode 100644 index fbf14d3134..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShellCommand.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.messaging.RPCOps -import org.crsh.command.BaseCommand -import org.crsh.shell.impl.command.CRaSHSession - -/** - * Simply extends CRaSH BaseCommand to add easy access to the RPC ops class. - */ -internal abstract class InteractiveShellCommand : BaseCommand() { - - abstract val rpcOpsClass: Class - - @Suppress("UNCHECKED_CAST") - fun ops(): T { - val cRaSHSession = context.session as CRaSHSession - val authInfo = cRaSHSession.authInfo as SshAuthInfo - return authInfo.getOrCreateRpcOps(rpcOpsClass) - } - - fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer - - fun isSsh() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).isSsh -} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsProducer.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsProducer.kt deleted file mode 100644 index 8b60087485..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/RPCOpsProducer.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.tools.shell - -import net.corda.client.rpc.CordaRPCClient -import net.corda.client.rpc.CordaRPCClientConfiguration -import net.corda.client.rpc.GracefulReconnect -import net.corda.client.rpc.RPCConnection -import net.corda.client.rpc.internal.RPCClient -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps - -internal interface RPCOpsProducer { - /** - * Returns [RPCConnection] of underlying proxy. Proxy can be obtained at any time by calling [RPCConnection.proxy] - */ - operator fun invoke(username: String?, credential: String?, rpcOpsClass: Class) : RPCConnection -} - -internal class DefaultRPCOpsProducer(private val configuration: ShellConfiguration, private val classLoader: ClassLoader? = null, private val standalone: Boolean) : RPCOpsProducer { - - override fun invoke(username: String?, credential: String?, rpcOpsClass: Class): RPCConnection { - - return if (rpcOpsClass == CordaRPCOps::class.java) { - // For CordaRPCOps we are using CordaRPCClient - val connection = if (standalone) { - CordaRPCClient( - configuration.hostAndPort, - configuration.ssl, - classLoader - ).start(username!!, credential!!, gracefulReconnect = GracefulReconnect()) - } else { - CordaRPCClient( - hostAndPort = configuration.hostAndPort, - configuration = CordaRPCClientConfiguration.DEFAULT.copy( - maxReconnectAttempts = 1 - ), - sslConfiguration = configuration.ssl, - classLoader = classLoader - ).start(username!!, credential!!) - } - @Suppress("UNCHECKED_CAST") - connection as RPCConnection - } else { - // For other types "plain" RPCClient is used - val rpcClient = RPCClient(configuration.hostAndPort, configuration.ssl) - val connection = rpcClient.start(rpcOpsClass, username!!, credential!!) - connection - } - } -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt deleted file mode 100644 index 6397db484e..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/SerializationSupport.kt +++ /dev/null @@ -1,93 +0,0 @@ -package net.corda.tools.shell - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.google.common.io.Closeables -import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.internal.copyTo -import net.corda.core.internal.inputStream -import org.crsh.command.InvocationContext -import rx.Observable -import java.io.BufferedInputStream -import java.io.InputStream -import java.nio.file.Paths -import java.util.* - -//region Extra serializers -// -// These serializers are used to enable the user to specify objects that aren't natural data containers in the shell, -// and for the shell to print things out that otherwise wouldn't be usefully printable. - -object ObservableSerializer : JsonSerializer>() { - override fun serialize(value: Observable<*>, gen: JsonGenerator, serializers: SerializerProvider) { - gen.writeString("(observable)") - } -} - -/** - * String value deserialized to [UniqueIdentifier]. - * Any string value used as [UniqueIdentifier.externalId]. - * If string contains underscore(i.e. externalId_uuid) then split with it. - * Index 0 as [UniqueIdentifier.externalId] - * Index 1 as [UniqueIdentifier.id] - * */ -object UniqueIdentifierDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UniqueIdentifier { - //Check if externalId and UUID may be separated by underscore. - if (p.text.contains("_")) { - val ids = p.text.split("_") - //Create UUID object from string. - val uuid: UUID = UUID.fromString(ids[1]) - //Create UniqueIdentifier object using externalId and UUID. - return UniqueIdentifier(ids[0], uuid) - } - //Any other string used as externalId. - return UniqueIdentifier.fromString(p.text) - } -} - -// An InputStream found in a response triggers a request to the user to provide somewhere to save it. -object InputStreamSerializer : JsonSerializer() { - var invokeContext: InvocationContext<*>? = null - - override fun serialize(value: InputStream, gen: JsonGenerator, serializers: SerializerProvider) { - - value.use { - val toPath = invokeContext!!.readLine("Path to save stream to (enter to ignore): ", true) - if (toPath == null || toPath.isBlank()) { - gen.writeString("") - } else { - val path = Paths.get(toPath) - it.copyTo(path) - gen.writeString("") - } - } - } -} - -// A file name is deserialized to an InputStream if found. -object InputStreamDeserializer : JsonDeserializer() { - // Keep track of them so we can close them later. - private val streams = Collections.synchronizedSet(HashSet()) - - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): InputStream { - val stream = object : BufferedInputStream(Paths.get(p.text).inputStream()) { - override fun close() { - super.close() - streams.remove(this) - } - } - streams += stream - return stream - } - - fun closeAll() { - // Clone the set with toList() here so each closed stream can be removed from the set inside close(). - streams.toList().forEach { Closeables.closeQuietly(it) } - } -} -//endregion \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt deleted file mode 100644 index 76fd0f5f88..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.messaging.ClientRpcSslOptions -import java.nio.file.Path - -data class ShellConfiguration( - val commandsDirectory: Path, - val cordappsDirectory: Path? = null, - var user: String = "", - var password: String = "", - var permissions: Set? = null, - var localShellAllowExitInSafeMode: Boolean = false, - var localShellUnsafe: Boolean = false, - val hostAndPort: NetworkHostAndPort, - val ssl: ClientRpcSslOptions? = null, - val sshdPort: Int? = null, - val sshHostKeyDirectory: Path? = null, - val noLocalShell: Boolean = false) { - companion object { - const val COMMANDS_DIR = "shell-commands" - const val CORDAPPS_DIR = "cordapps" - const val SSHD_HOSTKEY_DIR = "ssh" - } -} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt deleted file mode 100644 index 94bebb8a46..0000000000 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/utlities/ANSIProgressRenderer.kt +++ /dev/null @@ -1,356 +0,0 @@ -package net.corda.tools.shell.utlities - -import net.corda.core.internal.Emoji -import net.corda.core.messaging.FlowProgressHandle -import net.corda.core.utilities.loggerFor -import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer.draw -import org.apache.commons.lang3.SystemUtils -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.core.LogEvent -import org.apache.logging.log4j.core.LoggerContext -import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender -import org.apache.logging.log4j.core.appender.ConsoleAppender -import org.apache.logging.log4j.core.appender.OutputStreamManager -import org.crsh.text.RenderPrintWriter -import org.fusesource.jansi.Ansi -import org.fusesource.jansi.Ansi.Attribute -import org.fusesource.jansi.AnsiConsole -import org.fusesource.jansi.AnsiOutputStream -import rx.Observable.combineLatest -import rx.Subscription -import java.util.* - -abstract class ANSIProgressRenderer { - - private var updatesSubscription: Subscription? = null - - protected var usingANSI = false - protected var checkEmoji = false - private val usingUnicode = !SystemUtils.IS_OS_WINDOWS - - private var treeIndex: Int = 0 - private var treeIndexProcessed: MutableSet = mutableSetOf() - protected var tree: List = listOf() - - private var installedYet = false - - private var onDone: () -> Unit = {} - - // prevMessagePrinted is just for non-ANSI mode. - private var prevMessagePrinted: String? = null - // prevLinesDraw is just for ANSI mode. - protected var prevLinesDrawn = 0 - - data class ProgressStep(val level: Int, val description: String, val parentIndex: Int?) - data class InputTreeStep(val level: Int, val description: String) - - private fun done(error: Throwable?) { - if (error == null) renderInternal(null) - draw(true, error) - onDone() - } - - fun render(flowProgressHandle: FlowProgressHandle<*>, onDone: () -> Unit = {}) { - this.onDone = onDone - renderInternal(flowProgressHandle) - } - - protected abstract fun printLine(line:String) - - protected abstract fun printAnsi(ansi:Ansi) - - protected abstract fun setup() - - private fun renderInternal(flowProgressHandle: FlowProgressHandle<*>?) { - updatesSubscription?.unsubscribe() - treeIndex = 0 - treeIndexProcessed.clear() - tree = listOf() - - if (!installedYet) { - setup() - installedYet = true - } - - prevMessagePrinted = null - prevLinesDrawn = 0 - draw(true) - - val treeUpdates = flowProgressHandle?.stepsTreeFeed?.updates - val indexUpdates = flowProgressHandle?.stepsTreeIndexFeed?.updates - - if (treeUpdates == null || indexUpdates == null) { - renderInBold("Cannot print progress for this flow as the required data is missing", Ansi()) - } else { - // By combining the two observables, a race condition where both emit items at roughly the same time is avoided. This could - // result in steps being incorrectly marked as skipped. Instead, whenever either observable emits an item, a pair of the - // last index and last tree is returned, which ensures that updates to either are processed in series. - updatesSubscription = combineLatest(treeUpdates, indexUpdates) { tree, index -> Pair(tree, index) }.subscribe( - { - val newTree = transformTree(it.first.map { elem -> InputTreeStep(elem.first, elem.second) }) - // Process indices first, as if the tree has changed the associated index with this update is for the old tree. Note - // that the one case where this isn't true is the very first update, but in this case the index should be 0 (as this - // update is for the initial state). The remapping on a new tree assumes the step at index 0 is always at least current, - // so this case is handled there. - treeIndex = it.second - treeIndexProcessed.add(it.second) - if (newTree != tree) { - remapIndices(newTree) - tree = newTree - } - draw(true) - }, - { done(it) }, - { done(null) } - ) - } - } - - // Create a new tree of steps that also holds a reference to the parent of each step. This is required to uniquely identify each step - // (assuming that each step label is unique at a given level). - private fun transformTree(inputTree: List): List { - if (inputTree.isEmpty()) { - return listOf() - } - val stack = Stack>() - stack.push(Pair(0, inputTree[0])) - return inputTree.mapIndexed { index, step -> - val parentIndex = try { - val top = stack.peek() - val levelDifference = top.second.level - step.level - if (levelDifference >= 0) { - // The top of the stack is at the same or lower level than the current step. Remove items from the top until the topmost - // item is at a higher level - this is the parent step. - repeat(levelDifference + 1) { stack.pop() } - } - stack.peek().first - } catch (e: EmptyStackException) { - // If there is nothing on the stack at any point, it implies that this step is at the top level and has no parent. - null - } - stack.push(Pair(index, step)) - ProgressStep(step.level, step.description, parentIndex) - } - } - - private fun remapIndices(newTree: List) { - val newIndices = newTree.filter { - treeIndexProcessed.contains(tree.indexOf(it)) - }.map { - newTree.indexOf(it) - }.toMutableSet() - treeIndex = newIndices.max() ?: 0 - treeIndexProcessed = if (newIndices.isNotEmpty()) newIndices else mutableSetOf(0) - } - - @Synchronized protected fun draw(moveUp: Boolean, error: Throwable? = null) { - - if (!usingANSI) { - val currentMessage = tree.getOrNull(treeIndex)?.description - if (currentMessage != null && currentMessage != prevMessagePrinted) { - printLine(currentMessage) - prevMessagePrinted = currentMessage - } - return - } - - fun printingBody() { - // Handle the case where the number of steps in a progress tracker is changed during execution. - val ansi = Ansi() - if (prevLinesDrawn > 0 && moveUp) - ansi.cursorUp(prevLinesDrawn) - - // Put a blank line between any logging and us. - ansi.eraseLine() - ansi.newline() - if (tree.isEmpty()) return - var newLinesDrawn = 1 + renderLevel(ansi, error != null) - - if (error != null) { - val errorIcon = if (usingUnicode) Emoji.skullAndCrossbones else "ERROR: " - - var errorToPrint = error - var indent = 0 - while (errorToPrint != null) { - ansi.fgRed() - ansi.a("${"\t".repeat(indent)}$errorIcon ${errorToPrint.message}") - ansi.newline() - errorToPrint = errorToPrint.cause - indent++ - } - ansi.reset() - - ansi.eraseLine(Ansi.Erase.FORWARD) - ansi.newline() - newLinesDrawn++ - } - - if (newLinesDrawn < prevLinesDrawn) { - // If some steps were removed from the progress tracker, we don't want to leave junk hanging around below. - val linesToClear = prevLinesDrawn - newLinesDrawn - repeat(linesToClear) { - ansi.eraseLine() - ansi.newline() - } - ansi.cursorUp(linesToClear) - } - prevLinesDrawn = newLinesDrawn - - printAnsi(ansi) - } - - if (checkEmoji) { - Emoji.renderIfSupported(::printingBody) - } else { - printingBody() - } - } - - // Returns number of lines rendered. - private fun renderLevel(ansi: Ansi, error: Boolean): Int { - with(ansi) { - var lines = 0 - for ((index, step) in tree.withIndex()) { - val processedStep = treeIndexProcessed.contains(index) - val skippedStep = index < treeIndex && !processedStep - val activeStep = index == treeIndex - - val marker = when { - activeStep -> if (usingUnicode) "${Emoji.rightArrow} " else "CURRENT: " - processedStep -> if (usingUnicode) " ${Emoji.greenTick} " else "DONE: " - skippedStep -> " " - error -> if (usingUnicode) "${Emoji.noEntry} " else "ERROR: " - else -> " " // Not reached yet. - } - a(" ".repeat(step.level)) - a(marker) - - when { - activeStep -> renderInBold(step.description, ansi) - skippedStep -> renderInFaint(step.description, ansi) - else -> a(step.description) - } - - eraseLine(Ansi.Erase.FORWARD) - newline() - lines++ - } - return lines - } - } - - private fun renderInBold(payload: String, ansi: Ansi) { - with(ansi) { - a(Attribute.INTENSITY_BOLD) - a(payload) - a(Attribute.INTENSITY_BOLD_OFF) - } - } - - private fun renderInFaint(payload: String, ansi: Ansi) { - with(ansi) { - a(Attribute.INTENSITY_FAINT) - a(payload) - a(Attribute.INTENSITY_BOLD_OFF) - } - } - -} - -class CRaSHANSIProgressRenderer(val renderPrintWriter:RenderPrintWriter) : ANSIProgressRenderer() { - - override fun printLine(line: String) { - renderPrintWriter.println(line) - } - - override fun printAnsi(ansi: Ansi) { - renderPrintWriter.print(ansi) - renderPrintWriter.flush() - } - - override fun setup() { - // We assume SSH always use ANSI. - usingANSI = true - } - - -} - -/** - * Knows how to render a [FlowProgressHandle] to the terminal using coloured, emoji-fied output. Useful when writing small - * command line tools, demos, tests etc. Just call [draw] method and it will go ahead and start drawing - * if the terminal supports it. Otherwise it just prints out the name of the step whenever it changes. - * - * When a progress tracker is on the screen, it takes over the bottom part and reconfigures logging so that, assuming - * 1 log event == 1 line, the progress tracker is always glued to the bottom and logging scrolls above it. - * - * TODO: More thread safety - */ -object StdoutANSIProgressRenderer : ANSIProgressRenderer() { - - override fun setup() { - AnsiConsole.systemInstall() - checkEmoji = true - - // This line looks weird as hell because the magic code to decide if we really have a TTY or not isn't - // actually exposed anywhere as a function (weak sauce). So we have to rely on our knowledge of jansi - // implementation details. - @Suppress("DEPRECATION") - usingANSI = AnsiConsole.wrapOutputStream(System.out) !is AnsiOutputStream - - if (usingANSI) { - // This super ugly code hacks into log4j and swaps out its console appender for our own. It's a bit simpler - // than doing things the official way with a dedicated plugin, etc, as it avoids mucking around with all - // the config XML and lifecycle goop. - val manager = LogManager.getContext(false) as LoggerContext - val consoleAppender = manager.configuration.appenders.values.filterIsInstance().singleOrNull { it.name == "Console-Selector" } - if (consoleAppender == null) { - loggerFor().warn("Cannot find console appender - progress tracking may not work as expected") - return - } - @Suppress("DEPRECATION") - val scrollingAppender = object : AbstractOutputStreamAppender( - consoleAppender.name, consoleAppender.layout, consoleAppender.filter, - consoleAppender.ignoreExceptions(), true, consoleAppender.manager) { - override fun append(event: LogEvent) { - // We lock on the renderer to avoid threads that are logging to the screen simultaneously messing - // things up. Of course this slows stuff down a bit, but only whilst this little utility is in use. - // Eventually it will be replaced with a real GUI and we can delete all this. - synchronized(StdoutANSIProgressRenderer) { - if (tree.isNotEmpty()) { - val ansi = Ansi.ansi() - repeat(prevLinesDrawn) { ansi.eraseLine().cursorUp(1).eraseLine() } - System.out.print(ansi) - System.out.flush() - } - - super.append(event) - - if (tree.isNotEmpty()) - draw(false) - } - } - } - scrollingAppender.start() - manager.configuration.appenders[consoleAppender.name] = scrollingAppender - val loggerConfigs = manager.configuration.loggers.values - for (config in loggerConfigs) { - val appenderRefs = config.appenderRefs - val consoleAppenders = config.appenders.filter { it.value is ConsoleAppender }.keys - consoleAppenders.forEach { config.removeAppender(it) } - appenderRefs.forEach { config.addAppender(manager.configuration.appenders[it.ref], it.level, it.filter) } - } - manager.updateLoggers() - } - } - - override fun printLine(line:String) { - System.out.println(line) - } - - override fun printAnsi(ansi: Ansi) { - // Need to force a flush here in order to ensure stderr/stdout sync up properly. - System.out.print(ansi) - System.out.flush() - } -} \ No newline at end of file diff --git a/tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy b/tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy deleted file mode 100644 index b3c44aa941..0000000000 --- a/tools/shell/src/main/resources/net/corda/tools/shell/base/login.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.tools.shell.base - -// Note that this file MUST be in a sub-directory called "base" relative to the path -// given in the configuration code in InteractiveShell. - -welcome = { -> - """ - - Welcome to the Corda interactive shell. - You can see the available commands by typing 'help'. - - """.stripIndent() -} - -prompt = { -> - return "${new Date()}>>> " -} diff --git a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java deleted file mode 100644 index 7eb54681c2..0000000000 --- a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java +++ /dev/null @@ -1,279 +0,0 @@ -package net.corda.tools.shell; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.common.collect.Lists; -import kotlin.Pair; -import net.corda.client.jackson.JacksonSupport; -import net.corda.client.jackson.internal.ToStringSerialize; -import net.corda.core.contracts.Amount; -import net.corda.core.crypto.SecureHash; -import net.corda.core.flows.FlowException; -import net.corda.core.flows.FlowLogic; -import net.corda.core.flows.FlowSession; -import net.corda.core.flows.StateMachineRunId; -import net.corda.core.identity.CordaX500Name; -import net.corda.core.identity.Party; -import net.corda.core.internal.concurrent.CordaFutureImplKt; -import net.corda.core.internal.concurrent.OpenFuture; -import net.corda.core.messaging.FlowProgressHandleImpl; -import net.corda.core.utilities.ProgressTracker; -import net.corda.coretesting.internal.InternalTestConstantsKt; -import net.corda.node.services.identity.InMemoryIdentityService; -import net.corda.testing.core.TestIdentity; -import org.jetbrains.annotations.Nullable; -import org.junit.Test; -import rx.Observable; - -import java.util.*; - -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class InteractiveShellJavaTest { - private static TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); - - // should guarantee that FlowA will have synthetic method to access this field - private static final String synthetic = "synth"; - - private static final boolean IS_OPENJ9 = System.getProperty("java.vm.name").toLowerCase().contains("openj9"); - - abstract static class StringFlow extends FlowLogic { - abstract String getA(); - } - - @SuppressWarnings("unused") - public static class FlowA extends StringFlow { - - private String a; - - public FlowA(String a) { - if (!synthetic.isEmpty()) { - this.a = a; - } - } - - public FlowA(int b) { - this(Integer.valueOf(b).toString()); - } - - public FlowA(Integer b, String c) { - this(b.toString() + c); - } - - public FlowA(Amount amount) { - this(amount.toString()); - } - - public FlowA(Pair, SecureHash.SHA256> pair) { - this(pair.toString()); - } - - public FlowA(Party party) { - this(party.getName().toString()); - } - - public FlowA(Integer b, Amount amount) { - this(String.format("%d %s", amount.getQuantity() + (b == null ? 0 : b), amount.getToken())); - } - - public FlowA(String[] b) { - this(String.join("+", b)); - } - - public FlowA(Amount[] amounts) { - this(String.join("++", Arrays.stream(amounts).map(Amount::toString).collect(toList()))); - } - - @Nullable - @Override - public ProgressTracker getProgressTracker() { - return new ProgressTracker(); - } - - @Override - public String call() { - return a; - } - - @Override - String getA() { - return a; - } - } - - public static class FlowB extends StringFlow { - - private Party party; - private String a; - - public FlowB(Party party, String a) { - this.party = party; - this.a = a; - } - - public FlowB(Amount amount, int abc) { - } - - @Nullable - @Override - public ProgressTracker getProgressTracker() { - return new ProgressTracker(); - } - - @Override - public String call() throws FlowException { - FlowSession session = initiateFlow(party); - - - Integer integer = session.receive(Integer.class).unwrap((i) -> i); - - return integer.toString(); - - } - - @Override - String getA() { - return a; - } - } - - @ToStringSerialize - public static class UserValue { - private final String label; - - public UserValue(@JsonProperty("label") String label) { - this.label = label; - } - - @SuppressWarnings("unused") // Used via reflection. - public String getLabel() { - return label; - } - - @Override - public String toString() { - return label; - } - } - - private InMemoryIdentityService ids = new InMemoryIdentityService(Lists.newArrayList(megaCorp.getIdentity()), InternalTestConstantsKt.getDEV_ROOT_CA().getCertificate()); - - private ObjectMapper om = JacksonSupport.createInMemoryMapper(ids, new YAMLFactory()); - - private String output; - - private void check(String input, String expected, Class flowClass) throws InteractiveShell.NoApplicableConstructor { - InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> { - StringFlow instance = null; - try { - instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args); - } catch (Exception e) { - System.out.println(e); - throw new RuntimeException(e); - } - output = instance.getA(); - OpenFuture future = CordaFutureImplKt.openFuture(); - future.set("ABC"); - return new FlowProgressHandleImpl(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); - }, input, flowClass, om); - assertEquals(input, expected, output); - } - - @Test - public void flowStartSimple() throws InteractiveShell.NoApplicableConstructor { - check("a: Hi there", "Hi there", FlowA.class); - if (!IS_OPENJ9) { - check("b: 12", "12", FlowA.class); - check("b: 12, c: Yo", "12Yo", FlowA.class); - } - } - - @Test - public void flowStartWithComplexTypes() throws InteractiveShell.NoApplicableConstructor { - check("amount: £10", "10.00 GBP", FlowA.class); - } - - @Test - public void flowStartWithNestedTypes() throws InteractiveShell.NoApplicableConstructor { - check( - "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", - "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)", - FlowA.class); - } - - @Test - public void flowStartWithUserAmount() throws InteractiveShell.NoApplicableConstructor { - check( - "b: 500, amount: { \"quantity\": 10001, \"token\":{ \"label\": \"of value\" } }", - "10501 of value", - FlowA.class); - } - - @Test - public void flowStartWithArrayType() throws InteractiveShell.NoApplicableConstructor { - if (!IS_OPENJ9) { - check( - "b: [ One, Two, Three, Four ]", - "One+Two+Three+Four", - FlowA.class - ); - } - } - - @Test - public void flowStartWithArrayOfNestedType() throws InteractiveShell.NoApplicableConstructor { - check( - "amounts: [ { \"quantity\": 10, \"token\": { \"label\": \"(1)\" } }, { \"quantity\": 200, \"token\": { \"label\": \"(2)\" } } ]", - "10 (1)++200 (2)", - FlowA.class - ); - } - - @Test(expected = InteractiveShell.NoApplicableConstructor.class) - public void flowStartNoArgs() throws InteractiveShell.NoApplicableConstructor { - check("", "", FlowA.class); - } - - @Test(expected = InteractiveShell.NoApplicableConstructor.class) - public void flowMissingParam() throws InteractiveShell.NoApplicableConstructor { - check("c: Yo", "", FlowA.class); - } - - @Test(expected = InteractiveShell.NoApplicableConstructor.class) - public void flowTooManyParams() throws InteractiveShell.NoApplicableConstructor { - check("b: 12, c: Yo, d: Bar", "", FlowA.class); - } - - @Test - public void party() throws InteractiveShell.NoApplicableConstructor { - check("party: \"" + megaCorp.getName() + "\"", megaCorp.getName().toString(), FlowA.class); - } - - @Test - public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor { - check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class); - } - - @Test - public void niceErrors() { - // Most cases are checked in the Kotlin test, so we only check raw types here. - try { - check("amount: $100", "", FlowB.class); - } catch (InteractiveShell.NoApplicableConstructor e) { - assertEquals("[amount: Amount, abc: int]: missing parameter abc", e.getErrors().get(1)); - } - } - - @Test - public void flowStartWithUnknownParty() throws InteractiveShell.NoApplicableConstructor { - try { - check("party: nonexistent", "", FlowA.class); - } catch (InteractiveShell.NoApplicableConstructor e) { - assertTrue(e.getErrors().get(0).contains("No matching Party found")); - assertEquals(1, e.getErrors().size()); - } - } -} diff --git a/tools/shell/src/test/java/net/corda/tools/shell/OutputFormatCommandTest.java b/tools/shell/src/test/java/net/corda/tools/shell/OutputFormatCommandTest.java deleted file mode 100644 index 2186c59832..0000000000 --- a/tools/shell/src/test/java/net/corda/tools/shell/OutputFormatCommandTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.corda.tools.shell; - -import org.crsh.command.InvocationContext; -import org.crsh.command.ScriptException; -import org.crsh.text.RenderPrintWriter; -import org.junit.Before; -import org.junit.Test; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class OutputFormatCommandTest { - - @SuppressWarnings("unchecked") - private InvocationContext mockInvocationContext = mock(InvocationContext.class); - private RenderPrintWriter printWriter; - - private OutputFormatCommand outputFormatCommand; - - private static final String JSON_FORMAT_STRING = "json"; - private static final String YAML_FORMAT_STRING = "yaml"; - - @Before - public void setup() { - printWriter = mock(RenderPrintWriter.class); - outputFormatCommand = new OutputFormatCommand(printWriter); - } - - @Test - public void testValidUpdateToJson() { - outputFormatCommand.set(mockInvocationContext, JSON_FORMAT_STRING); - outputFormatCommand.get(mockInvocationContext); - - verify(printWriter).println(JSON_FORMAT_STRING); - } - - @Test - public void testValidUpdateToYaml() { - outputFormatCommand.set(mockInvocationContext, YAML_FORMAT_STRING); - outputFormatCommand.get(mockInvocationContext); - - verify(printWriter).println(YAML_FORMAT_STRING); - } - - @Test - public void testInvalidUpdate() { - assertThatExceptionOfType(ScriptException.class).isThrownBy(() -> outputFormatCommand.set(mockInvocationContext, "some-invalid-format")) - .withMessage("The provided format is not supported: some-invalid-format"); - } -} diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt deleted file mode 100644 index 60823aebd4..0000000000 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/CustomTypeJsonParsingTests.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.tools.shell - -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.module.kotlin.readValue -import net.corda.core.contracts.UniqueIdentifier -import org.junit.Before -import org.junit.Test -import java.util.* -import kotlin.test.assertEquals - -class CustomTypeJsonParsingTests { - lateinit var objectMapper: ObjectMapper - - //Dummy classes for testing. - data class State(val linearId: UniqueIdentifier) { - constructor() : this(UniqueIdentifier("required-for-json-deserializer")) - } - - data class UuidState(val uuid: UUID) { - //Default constructor required for json deserializer. - constructor() : this(UUID.randomUUID()) - } - - @Before - fun setup() { - objectMapper = ObjectMapper() - val simpleModule = SimpleModule() - simpleModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) - objectMapper.registerModule(simpleModule) - } - - @Test(timeout=300_000) - fun `Deserializing UniqueIdentifier by parsing string`() { - val id = "26b37265-a1fd-4c77-b2e0-715917ef619f" - val json = """{"linearId":"$id"}""" - val state = objectMapper.readValue(json) - - assertEquals(id, state.linearId.id.toString()) - } - - @Test(timeout=300_000) - fun `Deserializing UniqueIdentifier by parsing string with underscore`() { - val json = """{"linearId":"extkey564_26b37265-a1fd-4c77-b2e0-715917ef619f"}""" - val state = objectMapper.readValue(json) - - assertEquals("extkey564", state.linearId.externalId) - assertEquals("26b37265-a1fd-4c77-b2e0-715917ef619f", state.linearId.id.toString()) - } - - @Test(expected = JsonMappingException::class, timeout=300_000) - fun `Deserializing by parsing string contain invalid uuid with underscore`() { - val json = """{"linearId":"extkey564_26b37265-a1fd-4c77-b2e0"}""" - objectMapper.readValue(json) - } - - @Test(timeout=300_000) - fun `Deserializing UUID by parsing string`() { - val json = """{"uuid":"26b37265-a1fd-4c77-b2e0-715917ef619f"}""" - val state = objectMapper.readValue(json) - - assertEquals("26b37265-a1fd-4c77-b2e0-715917ef619f", state.uuid.toString()) - } - - @Test(expected = JsonMappingException::class, timeout=300_000) - fun `Deserializing UUID by parsing invalid uuid string`() { - val json = """{"uuid":"26b37265-a1fd-4c77-b2e0"}""" - objectMapper.readValue(json) - } -} \ No newline at end of file diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt deleted file mode 100644 index 15b4e951d8..0000000000 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/HashLookupCommandTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package net.corda.tools.shell - -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 -import net.corda.core.flows.StateMachineRunId -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.StateMachineTransactionMapping -import org.hamcrest.MatcherAssert -import org.hamcrest.core.StringContains -import org.junit.Test -import org.mockito.Mockito -import java.io.CharArrayWriter -import java.io.PrintWriter -import java.util.UUID -import kotlin.test.assertFailsWith - -class HashLookupCommandTest { - companion object { - private val DEFAULT_TXID: SecureHash = SecureHash.randomSHA256() - - private fun ops(vararg txIds: SecureHash): CordaRPCOps? { - val snapshot: List = txIds.map { txId -> - StateMachineTransactionMapping(StateMachineRunId(UUID.randomUUID()), txId) - } - return Mockito.mock(CordaRPCOps::class.java).apply { - Mockito.`when`(stateMachineRecordedTransactionMappingSnapshot()).thenReturn(snapshot) - } - } - - private fun runCommand(ops: CordaRPCOps?, txIdHash: String): String { - val arrayWriter = CharArrayWriter() - return PrintWriter(arrayWriter).use { - HashLookupShellCommand.hashLookup(it, ops, txIdHash) - it.flush() - arrayWriter.toString() - } - } - } - - @Test(timeout=300_000) - fun `hash lookup command returns correct response`() { - val ops = ops(DEFAULT_TXID) - var response = runCommand(ops, DEFAULT_TXID.toString()) - - MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID")) - - // Verify the hash of the TX ID also works - response = runCommand(ops, DEFAULT_TXID.sha256().toString()) - MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID")) - } - - @Test(timeout=300_000) - fun `should reject invalid txid`() { - val ops = ops(DEFAULT_TXID) - assertFailsWith("The provided string is not a valid hexadecimal SHA-256 hash value") { - runCommand(ops, "abcdefgh") - } - } - - @Test(timeout=300_000) - fun `should reject unknown txid`() { - val ops = ops(DEFAULT_TXID) - assertFailsWith("No matching transaction found") { - runCommand(ops, SecureHash.randomSHA256().toString()) - } - } -} \ No newline at end of file diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt deleted file mode 100644 index 99089e0079..0000000000 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ /dev/null @@ -1,284 +0,0 @@ -package net.corda.tools.shell - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.type.TypeFactory -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever -import net.corda.client.jackson.JacksonSupport -import net.corda.client.jackson.internal.ToStringSerialize -import net.corda.core.contracts.Amount -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.generateKeyPair -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StateMachineRunId -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.FlowProgressHandleImpl -import net.corda.core.node.NodeInfo -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.ProgressTracker -import net.corda.coretesting.internal.DEV_ROOT_CA -import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.getTestPartyAndCertificate -import org.crsh.command.InvocationContext -import org.crsh.text.Color -import org.crsh.text.Decoration -import org.crsh.text.RenderPrintWriter -import org.junit.Before -import org.junit.Test -import rx.Observable -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class InteractiveShellTest { - lateinit var inputObjectMapper: ObjectMapper - lateinit var cordaRpcOps: CordaRPCOps - lateinit var invocationContext: InvocationContext> - lateinit var printWriter: RenderPrintWriter - - @Before - fun setup() { - inputObjectMapper = objectMapperWithClassLoader(InteractiveShell.getCordappsClassloader()) - cordaRpcOps = mock() - invocationContext = mock() - printWriter = mock() - } - - companion object { - private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) - - private val ALICE = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public) - private val BOB = getTestPartyAndCertificate(BOB_NAME, generateKeyPair().public) - private val ALICE_NODE_INFO = NodeInfo(listOf(NetworkHostAndPort("localhost", 8080)), listOf(ALICE), 1, 1) - private val BOB_NODE_INFO = NodeInfo(listOf(NetworkHostAndPort("localhost", 80)), listOf(BOB), 1, 1) - private val NODE_INFO_JSON_PAYLOAD = - """ - { - "addresses" : [ "localhost:8080" ], - "legalIdentitiesAndCerts" : [ "O=Alice Corp, L=Madrid, C=ES" ], - "platformVersion" : 1, - "serial" : 1 - } - """.trimIndent() - private val NODE_INFO_YAML_PAYLOAD = - """ - addresses: - - "localhost:8080" - legalIdentitiesAndCerts: - - "O=Alice Corp, L=Madrid, C=ES" - platformVersion: 1 - serial: 1 - - """.trimIndent() - private val NETWORK_MAP_JSON_PAYLOAD = - """ - [ { - "addresses" : [ "localhost:8080" ], - "legalIdentitiesAndCerts" : [ "O=Alice Corp, L=Madrid, C=ES" ], - "platformVersion" : 1, - "serial" : 1 - }, { - "addresses" : [ "localhost:80" ], - "legalIdentitiesAndCerts" : [ "O=Bob Plc, L=Rome, C=IT" ], - "platformVersion" : 1, - "serial" : 1 - } ] - """.trimIndent() - private val NETWORK_MAP_YAML_PAYLOAD = - """ - - addresses: - - "localhost:8080" - legalIdentitiesAndCerts: - - "O=Alice Corp, L=Madrid, C=ES" - platformVersion: 1 - serial: 1 - - addresses: - - "localhost:80" - legalIdentitiesAndCerts: - - "O=Bob Plc, L=Rome, C=IT" - platformVersion: 1 - serial: 1 - - """.trimIndent() - } - - - private val ids = InMemoryIdentityService(listOf(megaCorp.identity), DEV_ROOT_CA.certificate) - @Suppress("DEPRECATION") - private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory()) - - private fun check(input: String, expected: String) { - var output: String? = null - InteractiveShell.runFlowFromString({ clazz, args -> - val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA - output = instance.a - val future = openFuture() - future.set("ABC") - FlowProgressHandleImpl(StateMachineRunId.createRandom(), future, Observable.just("Some string")) - }, input, FlowA::class.java, om) - assertEquals(expected, output!!, input) - } - - private fun objectMapperWithClassLoader(classLoader: ClassLoader?): ObjectMapper { - val objectMapper = JacksonSupport.createNonRpcMapper() - val tf = TypeFactory.defaultInstance().withClassLoader(classLoader) - objectMapper.typeFactory = tf - - return objectMapper - } - - @Test(timeout=300_000) - fun flowStartSimple() { - check("a: Hi there", "Hi there") - check("b: 12", "12") - check("b: 12, c: Yo", "12Yo") - } - - @Test(timeout=300_000) - fun flowStartWithComplexTypes() = check("amount: £10", "10.00 GBP") - - @Test(timeout=300_000) - fun flowStartWithNestedTypes() = check( - input = "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", - expected = "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)" - ) - - @Test(timeout=300_000) - fun flowStartWithArrayType() = check( - input = "c: [ One, Two, Three, Four ]", - expected = "One+Two+Three+Four" - ) - - @Test(timeout=300_000) - fun flowStartWithUserAmount() = check( - input = """b: 500, amount: { "quantity": 10001, "token":{ "label": "of value" } }""", - expected = "10501 of value" - ) - - @Test(timeout=300_000) - fun flowStartWithArrayOfNestedTypes() = check( - input = """amounts: [ { "quantity": 10, "token": { "label": "(1)" } }, { "quantity": 200, "token": { "label": "(2)" } } ]""", - expected = "10 (1)++200 (2)" - ) - - @Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000) - fun flowStartNoArgs() = check("", "") - - @Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000) - fun flowMissingParam() = check("d: Yo", "") - - @Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000) - fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") - - @Test(timeout=300_000) - fun niceTypeNamesInErrors() { - val e = assertFailsWith { - check("", expected = "") - } - val correct = setOf( - "[amounts: Amount[]]: missing parameter amounts", - "[amount: Amount]: missing parameter amount", - "[pair: Pair, SecureHash.SHA256>]: missing parameter pair", - "[party: Party]: missing parameter party", - "[b: Integer, amount: Amount]: missing parameter b", - "[c: String[]]: missing parameter c", - "[b: Integer, c: String]: missing parameter b", - "[a: String]: missing parameter a", - "[b: Integer]: missing parameter b" - ) - val errors = e.errors.toHashSet() - errors.removeAll(correct) - assert(errors.isEmpty()) { errors.joinToString(", ") } - } - - @Test(timeout=300_000) - fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString()) - - @Test(timeout=300_000) - fun runRpcFromStringWithCustomTypeResult() { - val command = listOf("nodeInfo") - whenever(cordaRpcOps.nodeInfo()).thenReturn(ALICE_NODE_INFO) - - InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.YAML) - InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper) - verify(printWriter).println(NODE_INFO_YAML_PAYLOAD) - - - InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.JSON) - InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper) - verify(printWriter).println(NODE_INFO_JSON_PAYLOAD.replace("\n", System.lineSeparator())) - } - - @Test(timeout=300_000) - fun runRpcFromStringWithCollectionsResult() { - val command = listOf("networkMapSnapshot") - whenever(cordaRpcOps.networkMapSnapshot()).thenReturn(listOf(ALICE_NODE_INFO, BOB_NODE_INFO)) - - InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.YAML) - InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper) - verify(printWriter).println(NETWORK_MAP_YAML_PAYLOAD) - - InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.JSON) - InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper) - verify(printWriter).println(NETWORK_MAP_JSON_PAYLOAD.replace("\n", System.lineSeparator())) - } - - @Test(timeout=300_000) - fun killFlowWithNonsenseID() { - InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om) - verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Decoration.bold, Color.red) - verify(printWriter).flush() - } - - @Test(timeout=300_000) - fun killFlowFailure() { - val runId = StateMachineRunId.createRandom() - whenever(cordaRpcOps.killFlow(any())).thenReturn(false) - - InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) - verify(cordaRpcOps).killFlow(runId) - verify(printWriter).println("Failed to kill flow $runId", Decoration.bold, Color.red) - verify(printWriter).flush() - } - - @Test(timeout=300_000) - fun killFlowSuccess() { - val runId = StateMachineRunId.createRandom() - whenever(cordaRpcOps.killFlow(any())).thenReturn(true) - - InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) - verify(cordaRpcOps).killFlow(runId) - verify(printWriter).println("Killed flow $runId", Decoration.bold, Color.yellow) - verify(printWriter).flush() - } -} - -@ToStringSerialize -data class UserValue(@JsonProperty("label") val label: String) { - override fun toString() = label -} - -@Suppress("UNUSED") -class FlowA(val a: String) : FlowLogic() { - constructor(b: Int?) : this(b.toString()) - constructor(b: Int?, c: String) : this(b.toString() + c) - constructor(amount: Amount) : this(amount.toString()) - constructor(pair: Pair, SecureHash.SHA256>) : this(pair.toString()) - constructor(party: Party) : this(party.name.toString()) - constructor(b: Int?, amount: Amount) : this("${(b ?: 0) + amount.quantity} ${amount.token}") - constructor(c: Array) : this(c.joinToString("+")) - constructor(amounts: Array>) : this(amounts.joinToString("++", transform = Amount::toString)) - - override val progressTracker = ProgressTracker() - override fun call() = a -} diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/utilities/ANSIProgressRendererTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/utilities/ANSIProgressRendererTest.kt deleted file mode 100644 index a86d443004..0000000000 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/utilities/ANSIProgressRendererTest.kt +++ /dev/null @@ -1,122 +0,0 @@ -package net.corda.tools.shell.utilities - -import com.nhaarman.mockito_kotlin.* -import net.corda.core.flows.StateMachineRunId -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.FlowProgressHandleImpl -import net.corda.tools.shell.utlities.ANSIProgressRenderer -import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer -import org.apache.commons.lang3.SystemUtils -import org.assertj.core.api.Assertions.assertThat -import org.crsh.text.RenderPrintWriter -import org.fusesource.jansi.Ansi -import org.junit.Before -import org.junit.Test -import rx.Observable -import rx.subjects.PublishSubject - -class ANSIProgressRendererTest { - - companion object { - private const val INTENSITY_BOLD_ON_ASCII = "[1m" - private const val INTENSITY_OFF_ASCII = "[22m" - private const val INTENSITY_FAINT_ON_ASCII = "[2m" - - private const val STEP_1_LABEL = "Running step 1" - private const val STEP_2_LABEL = "Running step 2" - private const val STEP_3_LABEL = "Running step 3" - private const val STEP_4_LABEL = "Running step 4" - private const val STEP_5_LABEL = "Running step 5" - - fun stepSuccess(stepLabel: String): String { - return if (SystemUtils.IS_OS_WINDOWS) """DONE: $stepLabel""" else """✓ $stepLabel""" - } - - fun stepSkipped(stepLabel: String): String { - return """ $INTENSITY_FAINT_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII""" - } - - fun stepActive(stepLabel: String): String { - return if (SystemUtils.IS_OS_WINDOWS) - """CURRENT: $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII""" - else - """▶︎ $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII""" - } - - fun stepNotRun(stepLabel: String): String { - return """ $stepLabel""" - } - } - - lateinit var printWriter: RenderPrintWriter - lateinit var progressRenderer: ANSIProgressRenderer - lateinit var indexSubject: PublishSubject - lateinit var feedSubject: PublishSubject>> - lateinit var flowProgressHandle: FlowProgressHandleImpl<*> - - @Before - fun setup() { - printWriter = mock() - progressRenderer = CRaSHANSIProgressRenderer(printWriter) - indexSubject = PublishSubject.create() - feedSubject = PublishSubject.create>>() - val stepsTreeIndexFeed = DataFeed(0, indexSubject) - val stepsTreeFeed = DataFeed>, List>>(listOf(), feedSubject) - flowProgressHandle = FlowProgressHandleImpl(StateMachineRunId.createRandom(), openFuture(), Observable.empty(), stepsTreeIndexFeed, stepsTreeFeed) - } - - private fun checkTrackingState(captor: KArgumentCaptor, updates: Int, trackerState: List) { - verify(printWriter, times(updates)).print(captor.capture()) - assertThat(captor.lastValue.toString()).containsSubsequence(trackerState) - verify(printWriter, times(updates)).flush() - } - - @Test(timeout=300_000) - fun `test that steps are rendered appropriately depending on their status`() { - progressRenderer.render(flowProgressHandle) - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL), Pair(0, STEP_3_LABEL))) - // The flow is currently at step 3, while step 1 has been completed and step 2 has been skipped. - indexSubject.onNext(0) - indexSubject.onNext(2) - - val captor = argumentCaptor() - checkTrackingState(captor, 2, listOf(stepSuccess(STEP_1_LABEL), stepSkipped(STEP_2_LABEL), stepActive(STEP_3_LABEL))) - } - - @Test(timeout=300_000) - fun `changing tree causes correct steps to be marked as done`() { - progressRenderer.render(flowProgressHandle) - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL))) - indexSubject.onNext(0) - indexSubject.onNext(1) - indexSubject.onNext(2) - - val captor = argumentCaptor() - checkTrackingState(captor, 3, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL))) - - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL))) - checkTrackingState(captor, 4, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_4_LABEL), stepNotRun(STEP_5_LABEL))) - } - - @Test(timeout=300_000) - fun `duplicate steps in different children handled correctly`() { - val captor = argumentCaptor() - progressRenderer.render(flowProgressHandle) - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL))) - indexSubject.onNext(0) - - checkTrackingState(captor, 1, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_2_LABEL))) - - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL))) - indexSubject.onNext(1) - indexSubject.onNext(2) - indexSubject.onNext(3) - - checkTrackingState(captor, 5, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL))) - - feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(2, STEP_4_LABEL))) - - checkTrackingState(captor, 6, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL), stepNotRun(STEP_4_LABEL))) - } -} \ No newline at end of file diff --git a/tools/shell/src/test/resources/config.conf b/tools/shell/src/test/resources/config.conf deleted file mode 100644 index e4446f1ec9..0000000000 --- a/tools/shell/src/test/resources/config.conf +++ /dev/null @@ -1,29 +0,0 @@ -node { - addresses { - rpc { - host : "alocalhost" - port : 1234 - } - } - user : demo - password : abcd1234 -} -extensions { - cordapps { - path : "/x/y/cordapps" - } - sshd { - enabled : "true" - port : 2223 - } - commands { - path : /x/y/commands - } -} -ssl { - truststore { - path : "/x/y/truststore.jks" - type : "JKS" - password : "pass2" - } - } \ No newline at end of file