mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge branch 'release/os/4.9' into merge-release/os/4.8-release/os/4.9-2023-11-19-6
This commit is contained in:
commit
18d98760b1
@ -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}
|
||||
RUN useradd -m ${USER}
|
||||
|
4
.ci/dev/forward-merge/Jenkinsfile
vendored
4
.ci/dev/forward-merge/Jenkinsfile
vendored
@ -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
|
||||
|
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -92,6 +92,7 @@ pipeline {
|
||||
}
|
||||
stage('Recompile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
|
30
.ci/dev/regression/Jenkinsfile
vendored
30
.ci/dev/regression/Jenkinsfile
vendored
@ -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-.*(?<!_JDK11)$/)
|
||||
boolean isInternalRelease = (env.TAG_NAME =~ /^internal-release-.*$/)
|
||||
boolean isReleaseCandidate = (env.TAG_NAME =~ /^(release-.*(RC|HC).*(?<!_JDK11))$/)
|
||||
boolean isReleasePatch = (env.TAG_NAME =~ /^release.*([1-9]\d*|0)(\.([1-9]\d*|0)){2}$/)
|
||||
def buildEdition = (isReleaseTag || isReleaseCandidate) ? "Corda Community Edition" : "Corda Open Source"
|
||||
|
||||
/**
|
||||
* Common Gradle arguments for all Gradle executions
|
||||
@ -27,7 +29,8 @@ String COMMON_GRADLE_PARAMS = [
|
||||
'--info',
|
||||
'-Pcompilation.warningsAsErrors=false',
|
||||
'-Ptests.failFast=true',
|
||||
'--build-cache'
|
||||
'--build-cache',
|
||||
'-DexcludeShell'
|
||||
].join(' ')
|
||||
|
||||
pipeline {
|
||||
@ -59,11 +62,13 @@ pipeline {
|
||||
CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}"
|
||||
CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}"
|
||||
CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key')
|
||||
CORDA_BUILD_EDITION = "${buildEdition}"
|
||||
CORDA_USE_CACHE = "corda-remotes"
|
||||
DOCKER_URL = "https://index.docker.io/v1/"
|
||||
EMAIL_RECIPIENTS = credentials('corda4-email-recipient')
|
||||
INTEGRATION_ID = credentials('snyk-artifactory-c4')
|
||||
SNYK_API_KEY = "c4-os-snyk" //Jenkins credential type: Snyk Api token
|
||||
SNYK_API_TOKEN = credentials('c4-os-snyk-api-token-secret') //Jenkins credential type: Secret text
|
||||
SNYK_TOKEN = credentials('c4-os-snyk-api-token-secret') //Jenkins credential type: Secret text
|
||||
C4_OS_SNYK_ORG_ID = credentials('corda4-os-snyk-org-id')
|
||||
}
|
||||
|
||||
@ -110,7 +115,7 @@ pipeline {
|
||||
expression { isReleaseTag || isReleaseCandidate || isReleaseBranch }
|
||||
}
|
||||
steps {
|
||||
snykLicenseGeneration(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID)
|
||||
snykLicenseGeneration(env.SNYK_TOKEN, env.C4_OS_SNYK_ORG_ID)
|
||||
}
|
||||
post {
|
||||
always {
|
||||
@ -274,7 +279,7 @@ pipeline {
|
||||
rtGradleRun(
|
||||
usesPlugin: true,
|
||||
useWrapper: true,
|
||||
switches: '-s --info',
|
||||
switches: '-s --info -DpublishApiDocs',
|
||||
tasks: 'artifactoryPublish',
|
||||
deployerId: 'deployer',
|
||||
buildName: env.ARTIFACTORY_BUILD_NAME
|
||||
@ -288,7 +293,7 @@ pipeline {
|
||||
|
||||
stage('Publish Release Candidate to Internal Repository') {
|
||||
when {
|
||||
expression { return false} // keeping stage to preserve Jenkins history on release branches, but not supported for patch builds pre 4.9
|
||||
expression { isReleaseCandidate }
|
||||
}
|
||||
steps {
|
||||
withCredentials([
|
||||
@ -310,7 +315,7 @@ pipeline {
|
||||
|
||||
stage('Publish Release to Docker Hub') {
|
||||
when {
|
||||
expression { isReleaseTag && !isInternalRelease && !isReleaseCandidate && !isReleasePatch}
|
||||
expression { isReleaseTag && !isInternalRelease && !isReleaseCandidate}
|
||||
}
|
||||
steps {
|
||||
withCredentials([
|
||||
@ -322,7 +327,7 @@ pipeline {
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
'docker:pushDockerImage',
|
||||
'-Pdocker.image.repository=corda/corda',
|
||||
'-Pdocker.image.repository=corda/community',
|
||||
'--image OFFICIAL'
|
||||
].join(' ')
|
||||
}
|
||||
@ -397,6 +402,13 @@ pipeline {
|
||||
if (isReleaseTag || isReleaseCandidate || isReleaseBranch) {
|
||||
snykSecurityScan.generateHtmlElements()
|
||||
}
|
||||
|
||||
if (isReleaseTag || isReleaseCandidate) {
|
||||
// auto import and scanning of Docker images tag is dictated by below properties, so retrieve these first to scan the approproate tag
|
||||
String cordaVersion = sh(script: 'grep "cordaVersion" constants.properties | awk -F= \'{print $2}\'', returnStdout: true).trim()
|
||||
String versionSuffix = sh(script: 'grep "versionSuffix" constants.properties | awk -F= \'{print $2}\'', returnStdout: true).trim()
|
||||
snykUtils.SnykApiImport(!versionSuffix.isEmpty() ? "${cordaVersion}-${versionSuffix}" : cordaVersion, SnykOrganisation.CORDA_4_OS, env.C4_OS_SNYK_ORG_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
unstable {
|
||||
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -3,9 +3,9 @@
|
||||
|
||||
# PR Checklist:
|
||||
|
||||
- [ ] Have you run the unit, integration and smoke tests as described [here](https://docs.corda.net/head/testing.html)?
|
||||
- [ ] If you added public APIs, did you write the JavaDocs?
|
||||
- [ ] If the changes are of interest to application developers, have you added them to the [changelog](https://docs.corda.net/head/changelog.html) (`/docs/source/changelog.rst`), and potentially the [release notes](https://docs.corda.net/head/release-notes.html) (`/docs/source/release-notes.rst`)?
|
||||
- [ ] If you are contributing for the first time, please read the [contributor agreement](https://docs.corda.net/head/contributing.html) now and add a comment to this pull request stating that your PR is in accordance with the [Developer's Certificate of Origin](https://docs.corda.net/head/contributing.html#merging-the-changes-back-into-corda).
|
||||
- [ ] Have you run the unit, integration and smoke tests as described [here](https://docs.r3.com/en/platform/corda/4.8/open-source/testing.html)?
|
||||
- [ ] If you added public APIs, did you write the JavaDocs/kdocs?
|
||||
- [ ] If the changes are of interest to application developers, have you added them to the changelog, and potentially the [release notes](https://docs.corda.net/head/release-notes.html) (`https://docs.r3.com/en/platform/corda/4.8/open-source/release-notes.html`)?
|
||||
- [ ] If you are contributing for the first time, please read the [contributor agreement](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html) now and add a comment to this pull request stating that your PR is in accordance with the [Developer's Certificate of Origin](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html#merging-the-changes-back-into-corda).
|
||||
|
||||
Thanks for your code, it's appreciated! :)
|
||||
|
231
.snyk
Normal file
231
.snyk
Normal file
@ -0,0 +1,231 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.25.0
|
||||
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
|
||||
ignore:
|
||||
SNYK-JAVA-COMGOOGLEGUAVA-1015415:
|
||||
- '*':
|
||||
reason: >-
|
||||
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: {}
|
@ -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).
|
||||
|
@ -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
|
||||
|
||||
|
37
build.gradle
37
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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}"
|
||||
|
@ -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<Any>()
|
||||
consumer.setMessageHandler {
|
||||
|
@ -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<String, Any?> = 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<Any>(nonExistentClientId)
|
||||
assertNull(flowHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class SimpleFlow : FlowLogic<Int>() {
|
||||
|
||||
|
@ -98,6 +98,8 @@ class RPCClient<I : RPCOps>(
|
||||
// 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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -393,10 +393,11 @@ class ReconnectingCordaRPCOps private constructor(
|
||||
initialFeed.copy(updates = observable)
|
||||
}
|
||||
FlowHandleWithClientId::class.java -> {
|
||||
val initialHandle: FlowHandleWithClientId<Any?> = uncheckedCast(doInvoke(method, args,
|
||||
// initialHandle can be null. See @CordaRPCOps.reattachFlowWithClientId.
|
||||
val initialHandle: FlowHandleWithClientId<Any?>? = 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<Any?>()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.9-SNAPSHOT"
|
@ -135,7 +135,7 @@
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="100">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfFileName glob="${diagnostic-log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
@ -159,7 +159,7 @@
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="100">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfFileName glob="checkpoints_agent*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
@ -202,7 +202,15 @@
|
||||
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
|
||||
<Logger name="org.apache.activemq.artemis.core.server" level="warn" additivity="false">
|
||||
<Filters>
|
||||
<RegexFilter regex=".*AMQ222165.*" onMatch="DENY" onMismatch="NEUTRAL"/>
|
||||
<RegexFilter regex=".*AMQ222166.*" onMatch="DENY" onMismatch="NEUTRAL"/>
|
||||
</Filters>
|
||||
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
<Logger name="org.apache.activemq.audit" level="error" additivity="false">
|
||||
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1364,7 +1364,6 @@
|
||||
<ID>ThrowsCount:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String)</ID>
|
||||
<ID>ThrowsCount:AbstractNode.kt$AbstractNode$private fun installCordaServices()</ID>
|
||||
<ID>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()</ID>
|
||||
<ID>ThrowsCount:BrokerJaasLoginModule.kt$BaseBrokerJaasLoginModule$@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<javax.security.cert.X509Certificate>?></ID>
|
||||
<ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$ fun verifyCheckpointsCompatible( checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any> )</ID>
|
||||
<ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$// Throws exception when the flow is incompatible private fun checkFlowCompatible(subFlow: SubFlow, currentCordappsByHash: Map<SecureHash.SHA256, Cordapp>, platformVersion: Int)</ID>
|
||||
<ID>ThrowsCount:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema)</ID>
|
||||
|
@ -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<Identifier> 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())
|
||||
}
|
||||
|
||||
|
@ -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/*
|
||||
|
@ -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
|
||||
|
@ -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 && \
|
||||
|
@ -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 && \
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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<Packet>(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<AMQPMessage>(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<T : Any>(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?
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<String, Any?>()
|
||||
@ -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<String, Any?>()
|
||||
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()
|
||||
|
@ -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))
|
||||
|
@ -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<Pair<Boolean, Boolean>>()).apply {
|
||||
drainingModeWasChangedEvents = PublishSubject.create<Pair<Boolean, Boolean>>(),
|
||||
terminateOnConnectionError = false,
|
||||
timeoutConfig = P2PMessagingClient.TimeoutConfig(10.seconds, 10.seconds, 10.seconds)).apply {
|
||||
config.configureWithDevSSLCertificate()
|
||||
messagingClient = this
|
||||
}
|
||||
|
@ -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() })
|
||||
}
|
||||
}
|
||||
}
|
@ -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<StateAndRef<SingleOwnerState>>() {
|
||||
|
||||
@ -296,4 +392,136 @@ class FlowHospitalTest {
|
||||
setCause(SQLException("deadlock"))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class CreateTransactionFlow(private val peer: Party) : FlowLogic<StateAndRef<DummyState>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<DummyState> {
|
||||
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<Unit>() {
|
||||
@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<DummyState>,
|
||||
private val consumePeerError: Boolean
|
||||
) : FlowLogic<StateAndRef<DummyState>>() {
|
||||
|
||||
constructor(peer: Party, ref: StateAndRef<DummyState>): this(peer, ref, false)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<DummyState> {
|
||||
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<Unit>() {
|
||||
|
||||
companion object {
|
||||
var exceptionSeenInUserFlow = false
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val consumeError = session.receive<Boolean>().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<DummyState>) : FlowLogic<Unit>() {
|
||||
|
||||
@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<Unit>() {
|
||||
|
||||
companion object {
|
||||
var exceptionSeenInUserFlow = false
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val id = session.receive<SecureHash>().unwrap { it }
|
||||
try {
|
||||
subFlow(ReceiveFinalityFlow(session, id))
|
||||
} catch (e: UnexpectedFlowEndException) {
|
||||
exceptionSeenInUserFlow = true
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
/** The implementation of the [RPCOps] interfaces used by this node. */
|
||||
@Suppress("DEPRECATION")
|
||||
open fun makeRPCOps(cordappLoader: CordappLoader): List<RPCOps> {
|
||||
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<S>(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<S>(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
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 }
|
||||
}
|
@ -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<javax.security.cert.X509Certificate>?, password: String): Pair<String, List<RolePrincipal>> {
|
||||
fun requireTls(certificates: Array<javax.security.cert.X509Certificate>?) = requireNotNull(certificates) { "No client certificates presented." }
|
||||
private fun authenticateAndAuthorise(username: String, certificates: Array<X509Certificate>, password: String): Pair<String, List<RolePrincipal>> {
|
||||
fun requireTls(certificates: Array<X509Certificate>?) = 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<Principal>()
|
||||
|
||||
@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate
|
||||
protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<javax.security.cert.X509Certificate>?> {
|
||||
@Suppress("ThrowsCount")
|
||||
protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<X509Certificate>> {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
val certificateCallback = CertificateCallback()
|
||||
|
@ -13,7 +13,7 @@ sealed class CertificateChainCheckPolicy {
|
||||
|
||||
@FunctionalInterface
|
||||
interface Check {
|
||||
fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>)
|
||||
fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>)
|
||||
}
|
||||
|
||||
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<javax.security.cert.X509Certificate>) {
|
||||
override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
|
||||
// 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<javax.security.cert.X509Certificate>) {
|
||||
override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
|
||||
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<javax.security.cert.X509Certificate>) {
|
||||
override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
|
||||
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<javax.security.cert.X509Certificate>) {
|
||||
override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
|
||||
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<javax.security.cert.X509Certificate>) {
|
||||
override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
|
||||
if (!theirChain.any { certificate -> CordaX500Name.parse(certificate.subjectDN.name).commonName == username }) {
|
||||
throw CertificateException("Client certificate does not match login username.")
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<String, Any?>, 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -15,4 +15,6 @@ interface IdentityServiceInternal : IdentityService {
|
||||
fun verifyAndRegisterNewRandomIdentity(identity: PartyAndCertificate)
|
||||
|
||||
fun invalidateCaches(name: CordaX500Name) {}
|
||||
|
||||
fun archiveNamedIdentity(name:String, publicKeyHash: String?) {}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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>("User") {
|
||||
private val username by string().optional()
|
||||
@ -67,9 +68,32 @@ internal object UserSpec : Configuration.Specification<User>("User") {
|
||||
internal object SecurityConfigurationSpec : Configuration.Specification<SecurityConfiguration>("SecurityConfiguration") {
|
||||
private object AuthServiceSpec : Configuration.Specification<SecurityConfiguration.AuthService>("AuthService") {
|
||||
private object DataSourceSpec : Configuration.Specification<SecurityConfiguration.AuthService.DataSource>("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<SecurityConfiguration.AuthService.DataSource> {
|
||||
|
@ -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) {
|
@ -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
|
||||
)
|
||||
|
@ -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<String>("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<String>("publicKeyHash"), root.get<String>("owningKeyHash"))
|
||||
val matchName = cb.equal(root.get<String>("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<String>, 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<String>("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<String>("name"), name))
|
||||
session.createQuery(deleteQuery).executeUpdate()
|
||||
session.save(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash(name, publicKeyHash))
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
|
||||
override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<String, ProtocolManager<BaseInterceptor<*>>>?): Acceptor {
|
||||
protocolMap: MutableMap<String, ProtocolManager<BaseInterceptor<*>, 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<String, ProtocolManager<BaseInterceptor<*>>>?,
|
||||
protocolMap: MutableMap<String, ProtocolManager<BaseInterceptor<*>, 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<SSLEngine>(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<SSLEngine>(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())
|
||||
}
|
||||
|
||||
|
||||
|
@ -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<String, Any>): 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<String, Any>): 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)
|
||||
}
|
@ -24,6 +24,10 @@ class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val databa
|
||||
private val beingProcessedMessages = ConcurrentHashMap<DeduplicationId, MessageMeta>()
|
||||
private val processedMessages = createProcessedMessages(cacheFactory)
|
||||
|
||||
enum class Outcome {
|
||||
NEW, DUPLICATE, IN_FLIGHT
|
||||
}
|
||||
|
||||
private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> {
|
||||
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].
|
||||
*/
|
||||
|
@ -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<Pair<Boolean, Boolean>>,
|
||||
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<String>) {
|
||||
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<BridgeEntry> {
|
||||
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")
|
||||
|
@ -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<CordaX500Name>
|
||||
|
||||
@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<Party> 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<NodeInfo>
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<TxCacheValue>): 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()
|
||||
|
@ -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.
|
||||
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
@ -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<CoreQueueConfiguration> {
|
||||
private fun queueConfigurations(): List<QueueConfiguration> {
|
||||
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,
|
||||
|
@ -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<MappedSchema> = 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"
|
||||
}
|
||||
|
@ -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<StateMachineRunId, FlowFiber>()
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder:
|
||||
class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractState>,
|
||||
val contractStateTypeMappings: Map<String, Set<String>>,
|
||||
override val criteriaBuilder: CriteriaBuilder,
|
||||
val criteriaQuery: CriteriaQuery<Tuple>,
|
||||
val criteriaQuery: CriteriaQuery<*>,
|
||||
val vaultStates: Root<VaultSchemaV1.VaultStates>) : AbstractQueryCriteriaParser<QueryCriteria, IQueryCriteriaParser, Sort>(), IQueryCriteriaParser {
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
|
@ -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<out T>): Vault.Page<T> {
|
||||
val (query, stateTypes) = createQuery(criteria, contractStateType, sorting)
|
||||
val (criteriaQuery, criteriaParser) = buildCriteriaQuery<Tuple>(criteria, contractStateType, sorting)
|
||||
val query = getSession().createQuery(criteriaQuery)
|
||||
query.setResultWindow(paging)
|
||||
|
||||
val statesMetadata: MutableList<Vault.StateMetadata> = 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 <R> Query<R>.resultStream(paging: PageSpecification): Stream<R> {
|
||||
@ -765,19 +769,17 @@ class NodeVaultService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : ContractState> queryTotalStateCount(baseCriteria: QueryCriteria, contractStateType: Class<out T>): 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 <T : ContractState> queryTotalStateCount(criteria: QueryCriteria, contractStateType: Class<out T>): Long {
|
||||
val (criteriaQuery, criteriaParser) = buildCriteriaQuery<Long>(criteria, contractStateType, null)
|
||||
criteriaQuery.select(criteriaBuilder.count(criteriaParser.vaultStates))
|
||||
val query = getSession().createQuery(criteriaQuery)
|
||||
return query.singleResult
|
||||
}
|
||||
|
||||
private fun <T : ContractState> createQuery(criteria: QueryCriteria,
|
||||
contractStateType: Class<out T>,
|
||||
sorting: Sort?): Pair<Query<Tuple>, Vault.StateStatus> {
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
|
||||
private inline fun <reified T> buildCriteriaQuery(criteria: QueryCriteria,
|
||||
contractStateType: Class<out ContractState>,
|
||||
sorting: Sort?): Pair<CriteriaQuery<T>, 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,8 +32,10 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
protected class PendingKeyValue(val transactions: MutableSet<DatabaseTransaction>, val estimatedSize: Int)
|
||||
|
||||
protected abstract val cache: LoadingCache<K, Transactional<V>>
|
||||
protected val pendingKeys = ConcurrentHashMap<K, MutableSet<DatabaseTransaction>>()
|
||||
protected val pendingKeys = ConcurrentHashMap<K, PendingKeyValue>()
|
||||
|
||||
/**
|
||||
* Returns the value associated with the key, first loading that value from the storage if necessary.
|
||||
@ -85,7 +87,8 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
// 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<K, V, E, out EK>(
|
||||
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<K, V, E, out EK>(
|
||||
|
||||
protected fun transactionalLoadValue(key: K): Transactional<V> {
|
||||
// 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<K, V, E, out EK>(
|
||||
}
|
||||
|
||||
// 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<DatabaseTransaction>(0)
|
||||
newSet += databaseTransaction
|
||||
newSet
|
||||
PendingKeyValue(newSet, estimatedSize)
|
||||
} else {
|
||||
added = oldSet.add(databaseTransaction)
|
||||
oldSet
|
||||
value
|
||||
}
|
||||
}
|
||||
return added
|
||||
@ -262,12 +270,13 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
|
||||
// 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<K, V, E, out EK>(
|
||||
* 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<T> {
|
||||
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<K, V, E, out EK>(
|
||||
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<K, V, E, out EK>(
|
||||
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<K, V, E, out EK>(
|
||||
}
|
||||
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<K, T>(private val map: AppendOnlyPersistentMapBase<K, T, *, *>,
|
||||
private val key: K,
|
||||
val weight: Int,
|
||||
private val _readerValueLoader: () -> T?,
|
||||
private val _writerValueLoader: () -> T = { throw IllegalAccessException("No value loader provided") }) : Transactional<T>() {
|
||||
|
||||
@ -352,7 +370,7 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
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<K, V, E, out EK>(
|
||||
// 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<K, V, E, out EK>(
|
||||
fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
toPersistentEntity: (key: K, value: V) -> E,
|
||||
persistentEntityClass: Class<E>,
|
||||
weighingFunc: (K, Transactional<V>) -> Int
|
||||
private val weighingFunc: (K, V?) -> Int
|
||||
) : AppendOnlyPersistentMapBase<K, V, E, EK>(
|
||||
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<V> ->
|
||||
value.shallowSize + if (value is Transactional.InFlight<*, *>) {
|
||||
value.weight * 2
|
||||
} else {
|
||||
weighingFunc(key, value.peekableValue)
|
||||
}
|
||||
},
|
||||
loadFunction = { key: K -> transactionalLoadValue(key) })
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class InfrequentlyMutatedCache<K : Any, V : Any>(name: String, cacheFactory: Nam
|
||||
backingCache.invalidateAll()
|
||||
}
|
||||
|
||||
private fun invalidate(key: K, value: Wrapper.Invalidated<V>): Wrapper.Invalidated<V> {
|
||||
private fun invalidate(key: K, value: Wrapper.Invalidated<V>): Wrapper.Invalidated<V>? {
|
||||
val tx = contextTransactionOrNull
|
||||
value.invalidators.incrementAndGet()
|
||||
currentlyInvalid[key] = value
|
||||
@ -81,7 +81,10 @@ class InfrequentlyMutatedCache<K : Any, V : Any>(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
|
||||
}
|
||||
|
@ -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<Unit>()
|
||||
registerActivateCallback(object: ActivateCallback {
|
||||
override fun activationComplete() {
|
||||
startupFuture.complete(Unit)
|
||||
}
|
||||
})
|
||||
registerActivationFailureListener {
|
||||
startupFuture.completeExceptionally(it)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
startupFuture.getOrThrow()
|
||||
}
|
@ -0,0 +1 @@
|
||||
net.corda.node.services.messaging.NodeOpenSSLContextFactory
|
@ -0,0 +1 @@
|
||||
net.corda.node.services.messaging.NodeSSLContextFactory
|
@ -27,6 +27,7 @@
|
||||
<include file="migration/node-core.changelog-v14-data.xml"/>
|
||||
<include file="migration/node-core.changelog-v16.xml"/>
|
||||
<include file="migration/node-core.changelog-v20.xml"/>
|
||||
<include file="migration/node-core.changelog-v22.xml"/>
|
||||
|
||||
<!-- This must run after node-core.changelog-init.xml, to prevent database columns being created twice. -->
|
||||
<include file="migration/vault-schema.changelog-v9.xml"/>
|
||||
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="add_node_named_identities_table_back_in">
|
||||
<createTable tableName="node_named_identities">
|
||||
<column name="name" type="NVARCHAR(128)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="pk_hash" type="NVARCHAR(130)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -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<AMQPMessage>().also {
|
||||
doReturn(coreMessage.validatedUserID).whenever(it).getStringProperty(Message.HDR_VALIDATED_USER)
|
||||
}
|
||||
}
|
||||
|
||||
private val session = rigorousMock<ServerSession>().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<AMQPMessage>().also {
|
||||
doThrow(IllegalStateException("My exception")).whenever(it).getStringProperty(any<SimpleString>())
|
||||
}
|
||||
// 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<String, Any> {
|
||||
throw ActiveMQSecurityException("My security exception")
|
||||
}
|
||||
}
|
||||
|
@ -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 = ""
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<String>("publicKeyHash"))
|
||||
val matchName = cb.equal(root.get<String>("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<String>("publicKeyHash"), publicKeyHashToRemove))
|
||||
session.createQuery(deleteQuery).executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryNodeNamedIdentities(node: TestStartedNode, party: CordaX500Name, publicKeyHash: String): List<PersistentNetworkMapCache.PersistentPartyToPublicKeyHash> {
|
||||
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<String>("publicKeyHash"), publicKeyHash)
|
||||
val matchName = cb.equal(root.get<String>("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)
|
||||
|
@ -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 ->
|
||||
|
@ -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",
|
||||
|
@ -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<String>
|
||||
}
|
||||
|
||||
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 {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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<ContractState>(
|
||||
VaultQueryCriteria(externalIds = listOf(externalId)),
|
||||
paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
|
||||
)
|
||||
}
|
||||
assertThat(page.states).hasSize(1)
|
||||
assertThat(page.totalStatesAvailable).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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)) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user